From 7bf0aff50387c7aed2ca501d02b4fa4473243c80 Mon Sep 17 00:00:00 2001 From: Steffen Zellmer <151627820+Steffen025@users.noreply.github.com> Date: Fri, 27 Mar 2026 19:07:54 -0400 Subject: [PATCH 1/5] refactor(installer): remove GUI/wizard installer and update docs --- .opencode/PAIOpenCodeWizard.ts | 1252 ----------------- .opencode/tsconfig.json | 2 +- .prd/PRD-20260327-install-docs-cleanup.md | 77 + ANNOUNCEMENT-v2.0.0.md | 4 +- CHANGELOG.md | 34 +- INSTALL.md | 35 +- PAI-Install/README.md | 73 +- PAI-Install/cli/quick-install.ts | 4 +- PAI-Install/electron/main.js | 193 --- PAI-Install/electron/package-lock.json | 801 ----------- PAI-Install/electron/package.json | 12 - PAI-Install/engine/build-opencode.ts | 4 +- PAI-Install/engine/provider-models.ts | 2 +- PAI-Install/generate-welcome.ts | 114 -- PAI-Install/install.sh | 40 +- PAI-Install/main.ts | 82 -- PAI-Install/public/app.js | 702 --------- PAI-Install/public/assets/banner.png | Bin 1657884 -> 0 bytes .../assets/fonts/advocate_34_narr_reg.woff2 | Bin 24860 -> 0 bytes .../assets/fonts/advocate_54_wide_reg.woff2 | Bin 24776 -> 0 bytes .../assets/fonts/concourse_3_bold.woff2 | Bin 31004 -> 0 bytes .../assets/fonts/concourse_3_regular.woff2 | Bin 31212 -> 0 bytes .../assets/fonts/concourse_4_regular.woff2 | Bin 30852 -> 0 bytes .../assets/fonts/triplicate_t3_code_bold.ttf | Bin 129788 -> 0 bytes .../fonts/triplicate_t3_code_regular.ttf | Bin 129428 -> 0 bytes .../public/assets/fonts/valkyrie_a_bold.woff2 | Bin 30012 -> 0 bytes .../assets/fonts/valkyrie_a_regular.woff2 | Bin 29896 -> 0 bytes PAI-Install/public/assets/pai-icon.png | Bin 73389 -> 0 bytes PAI-Install/public/assets/pai-logo-wide.png | Bin 257067 -> 0 bytes PAI-Install/public/assets/pai-logo.png | Bin 73389 -> 0 bytes PAI-Install/public/assets/voice-female.mp3 | Bin 67753 -> 0 bytes PAI-Install/public/assets/voice-male.mp3 | Bin 69425 -> 0 bytes PAI-Install/public/assets/welcome.mp3 | Bin 39051 -> 0 bytes PAI-Install/public/assets/welcome.wav | Bin 805766 -> 0 bytes PAI-Install/public/index.html | 71 - PAI-Install/public/styles.css | 874 ------------ PAI-Install/web/routes.ts | 370 ----- PAI-Install/web/server.ts | 155 -- README.md | 42 +- RELEASE-v2.0.0.md | 2 +- UPGRADE.md | 2 +- docs/ADVANCED-SETUP.md | 2 +- docs/DEFERRED-FEATURES.md | 2 +- docs/LOCAL-ONLY-SETUP.md | 16 +- docs/MIGRATION.md | 40 +- docs/WINDOWS-COMPATIBILITY.md | 10 +- ...DR-005-configuration-dual-file-approach.md | 10 +- docs/epic/EPIC-v3.0-Synthesis-Architecture.md | 9 +- docs/epic/OPTIMIZED-PR-PLAN.md | 10 +- docs/epic/TODO-v3.0.md | 12 +- docs/epic/V3.0-COMPLETION-PLAN.md | 11 +- docs/epic/V3.0-RUNBOOK.md | 29 +- docs/providers/anthropic-max.md | 133 -- 53 files changed, 249 insertions(+), 4982 deletions(-) delete mode 100755 .opencode/PAIOpenCodeWizard.ts create mode 100644 .prd/PRD-20260327-install-docs-cleanup.md delete mode 100644 PAI-Install/electron/main.js delete mode 100644 PAI-Install/electron/package-lock.json delete mode 100644 PAI-Install/electron/package.json delete mode 100644 PAI-Install/generate-welcome.ts delete mode 100644 PAI-Install/main.ts delete mode 100644 PAI-Install/public/app.js delete mode 100644 PAI-Install/public/assets/banner.png delete mode 100755 PAI-Install/public/assets/fonts/advocate_34_narr_reg.woff2 delete mode 100755 PAI-Install/public/assets/fonts/advocate_54_wide_reg.woff2 delete mode 100755 PAI-Install/public/assets/fonts/concourse_3_bold.woff2 delete mode 100755 PAI-Install/public/assets/fonts/concourse_3_regular.woff2 delete mode 100755 PAI-Install/public/assets/fonts/concourse_4_regular.woff2 delete mode 100755 PAI-Install/public/assets/fonts/triplicate_t3_code_bold.ttf delete mode 100755 PAI-Install/public/assets/fonts/triplicate_t3_code_regular.ttf delete mode 100755 PAI-Install/public/assets/fonts/valkyrie_a_bold.woff2 delete mode 100755 PAI-Install/public/assets/fonts/valkyrie_a_regular.woff2 delete mode 100644 PAI-Install/public/assets/pai-icon.png delete mode 100644 PAI-Install/public/assets/pai-logo-wide.png delete mode 100644 PAI-Install/public/assets/pai-logo.png delete mode 100644 PAI-Install/public/assets/voice-female.mp3 delete mode 100644 PAI-Install/public/assets/voice-male.mp3 delete mode 100644 PAI-Install/public/assets/welcome.mp3 delete mode 100644 PAI-Install/public/assets/welcome.wav delete mode 100644 PAI-Install/public/index.html delete mode 100644 PAI-Install/public/styles.css delete mode 100644 PAI-Install/web/routes.ts delete mode 100644 PAI-Install/web/server.ts delete mode 100644 docs/providers/anthropic-max.md diff --git a/.opencode/PAIOpenCodeWizard.ts b/.opencode/PAIOpenCodeWizard.ts deleted file mode 100755 index 81119fd6..00000000 --- a/.opencode/PAIOpenCodeWizard.ts +++ /dev/null @@ -1,1252 +0,0 @@ -#!/usr/bin/env bun -/** - * PAI-OpenCode Installation Wizard v2.0 - * - * Simplified 6-step setup for Personal AI Infrastructure: - * - 4 presets: Anthropic, ZEN PAID, ZEN FREE, Ollama - * - OpenCode dev build as prerequisite - * - model_tiers support for cost-aware agent routing - * - Creates opencode.json, settings.json, and identity files - * - * Usage: - * bun run PAIOpenCodeWizard.ts # Run the interactive wizard - * bun run PAIOpenCodeWizard.ts --help # Show help and exit - */ - -import { existsSync, mkdirSync, readFileSync, writeFileSync, lstatSync, symlinkSync, unlinkSync, realpathSync, renameSync } from 'fs'; -import { join, dirname } from 'path'; -import { homedir, userInfo } from 'os'; -import { execSync } from 'child_process'; -import * as readline from 'readline'; -import { parse as parseYaml } from 'yaml'; - -// ANSI colors -const c = { - reset: '\x1b[0m', - bold: '\x1b[1m', - blue: '\x1b[38;2;59;130;246m', - green: '\x1b[38;2;34;197;94m', - yellow: '\x1b[38;2;234;179;8m', - red: '\x1b[38;2;239;68;68m', - cyan: '\x1b[38;2;6;182;212m', - gray: '\x1b[38;2;100;116;139m', - magenta: '\x1b[38;2;168;85;247m', - dim: '\x1b[2m', -}; - -// Paths -const HOME = homedir(); -const SCRIPT_DIR = dirname(new URL(import.meta.url).pathname); -const OPENCODE_DIR = SCRIPT_DIR; // Wizard lives inside .opencode/ -const PROJECT_ROOT = dirname(OPENCODE_DIR); -const PROFILES_DIR = join(OPENCODE_DIR, 'profiles'); - -// ============================================================================ -// PRESET CONFIGURATIONS -// ============================================================================ - -const PRESETS = [ - { - name: 'Anthropic', - id: 'anthropic', - profile: 'anthropic', - description: 'Claude models — best quality, works with API key or Anthropic subscription (Pro/Max)', - authType: 'apikey-or-oauth' as const, - authNote: 'Set ANTHROPIC_API_KEY in your .env file, or run /connect in OpenCode to authenticate with your Anthropic subscription.', - }, - { - name: 'ZEN PAID', - id: 'zen-paid', - profile: 'zen-paid', - description: 'Budget-friendly pay-as-you-go — privacy-preserving, no subscription', - authType: 'oauth' as const, - authNote: 'Run /connect in OpenCode → choose ZEN to authenticate.', - }, - { - name: 'ZEN FREE', - id: 'zen-free', - profile: 'zen', - description: 'Free community models — no cost, data may be used for improvement', - authType: 'none' as const, - authNote: 'No authentication needed. Free community models.', - }, - { - name: 'Ollama (Local)', - id: 'local', - profile: 'local', - description: 'Fully offline with local models — private, no API keys, customize models after install', - authType: 'none' as const, - authNote: 'No authentication needed. Make sure Ollama is running (ollama serve). Customize models in opencode.json.', - }, -]; - -interface PresetConfig { - PRINCIPAL_NAME: string; - TIMEZONE: string; - AI_NAME: string; - CATCHPHRASE: string; - PRESET: (typeof PRESETS)[number]; -} - -// ============================================================================ -// UTILITY FUNCTIONS -// ============================================================================ - -function print(msg: string) { console.log(msg); } -function printSuccess(msg: string) { print(` ${c.green}✓${c.reset} ${msg}`); } -function printWarning(msg: string) { print(` ${c.yellow}!${c.reset} ${msg}`); } -function printError(msg: string) { print(` ${c.red}✗${c.reset} ${msg}`); } -function printInfo(msg: string) { print(` ${c.gray}→${c.reset} ${msg}`); } - -function createReadline(): readline.Interface { - return readline.createInterface({ - input: process.stdin, - output: process.stdout, - }); -} - -async function prompt(question: string, defaultValue?: string): Promise { - const displayDefault = defaultValue ? ` ${c.gray}[${defaultValue}]${c.reset}` : ''; - const rl = createReadline(); - - return new Promise((resolve) => { - rl.question(` ${question}${displayDefault}: `, (answer) => { - rl.close(); - resolve(answer.trim() || defaultValue || ''); - }); - }); -} - -async function promptChoice(question: string, choices: string[], defaultIdx = 0): Promise { - print(` ${question}`); - choices.forEach((choice, i) => { - const marker = i === defaultIdx ? `${c.cyan}→${c.reset}` : ' '; - print(` ${marker} ${['A', 'B', 'C', 'D', 'E'][i]}. ${choice}`); - }); - - const rl = createReadline(); - return new Promise((resolve) => { - rl.question(` ${c.gray}Enter A-${String.fromCharCode(64 + choices.length)} [${String.fromCharCode(65 + defaultIdx)}]:${c.reset} `, (answer) => { - rl.close(); - const letter = answer.trim().toUpperCase(); - const num = letter.charCodeAt(0) - 65; - if (num >= 0 && num < choices.length) { - resolve(num); - } else { - resolve(defaultIdx); - } - }); - }); -} - -function execCommand(cmd: string, options: { cwd?: string; silent?: boolean } = {}): { success: boolean; output: string; error?: string } { - try { - const output = execSync(cmd, { - encoding: 'utf-8', - cwd: options.cwd, - stdio: options.silent ? 'pipe' : 'inherit', - }); - return { success: true, output: (output ?? '').trim() }; - } catch (err: any) { - return { success: false, output: '', error: err.message }; - } -} - -// ============================================================================ -// STEP 0: PREREQUISITES CHECK -// ============================================================================ - -interface PrereqCheck { - bun: boolean; - bunVersion: string; - git: boolean; -} - -function checkPrerequisites(): PrereqCheck { - print(''); - print(`${c.bold}Step 0: Prerequisites Check${c.reset}`); - print(`${c.gray}─────────────────────────────────────────────────${c.reset}`); - - const results: PrereqCheck = { bun: false, bunVersion: '', git: false }; - - // Check bun with version requirement (1.3.9+ required — matches OpenCode monorepo packageManager) - try { - const bunVersion = execSync('bun --version 2>/dev/null', { encoding: 'utf-8' }).trim(); - const versionMatch = bunVersion.match(/(\d+)\.(\d+)\.(\d+)/); - if (versionMatch) { - const major = parseInt(versionMatch[1]); - const minor = parseInt(versionMatch[2]); - const patch = parseInt(versionMatch[3]); - if (major > 1 || (major === 1 && minor > 3) || (major === 1 && minor === 3 && patch >= 9)) { - printSuccess(`Bun ${bunVersion} found`); - results.bun = true; - results.bunVersion = bunVersion; - } else { - printError(`Bun ${bunVersion} found — version 1.3.9+ required`); - printInfo('Upgrade Bun: bun upgrade'); - } - } - } catch { - printError('Bun not found'); - printInfo('Install Bun: curl -fsSL https://bun.sh/install | bash'); - } - - // Check git - try { - const gitVersion = execSync('git --version 2>/dev/null', { encoding: 'utf-8' }).trim(); - printSuccess(`${gitVersion} found`); - results.git = true; - } catch { - printError('Git not found'); - printInfo('Install Git: https://git-scm.com/downloads'); - } - - return results; -} - -// ============================================================================ -// STEP 1: BUILD OPENCODE FROM DEV SOURCE -// ============================================================================ - -async function buildOpenCode(): Promise { - print(''); - print(`${c.bold}Step 1: Build OpenCode from Dev Source${c.reset}`); - print(`${c.gray}─────────────────────────────────────────────────${c.reset}`); - print(''); - print(` ${c.cyan}PAI-OpenCode requires a custom OpenCode build with model tier support${c.reset}`); - print(` ${c.gray}(built from our fork using Bun's native compiler — no Go needed)${c.reset}`); - print(''); - - // Check if opencode is already installed - const whichResult = execCommand('which opencode', { silent: true }); - let existingVersion = ''; - - if (whichResult.success) { - const versionResult = execCommand('opencode --version', { silent: true }); - if (versionResult.success) { - existingVersion = versionResult.output; - printInfo(`Found OpenCode: ${existingVersion}`); - } - } - - print(''); - print(' Do you want to build OpenCode from the development branch?'); - print(''); - - const choices = [ - 'Yes, build now (Recommended)', - existingVersion ? `Skip — I already have a dev build (${existingVersion})` : 'Skip — I already have a dev build', - 'Skip — I\'ll use stable (model tiers won\'t work)', - ]; - - const choice = await promptChoice('', choices, 0); - - if (choice === 1) { - // Skip - user has dev build - printSuccess('Using existing OpenCode installation'); - return true; - } - - if (choice === 2) { - // Skip - will use stable - printWarning('Using stable release — model_tiers will not work'); - printInfo('To enable model tiers later, rebuild from dev source:'); - printInfo(' bun run .opencode/PAIOpenCodeWizard.ts'); - return true; - } - - // Build from dev source - print(''); - print(`${c.cyan}Building OpenCode from development branch...${c.reset}`); - print(`${c.gray}This uses Bun's native compiler to produce a standalone binary.${c.reset}`); - print(''); - - const buildDir = '/tmp/opencode-build'; - const cloneUrl = 'https://github.com/Steffen025/opencode.git'; - - try { - // Clean up any existing build - execCommand(`rm -rf ${buildDir}`, { silent: true }); - - // Clone the repo - printInfo('Cloning OpenCode repository...'); - const cloneResult = execCommand( - `git clone ${cloneUrl} ${buildDir}`, - { silent: false } - ); - - if (!cloneResult.success) { - printError('Failed to clone repository'); - return false; - } - - // Checkout feature/model-tiers branch (development branch with model tier support) - printInfo('Checking out feature/model-tiers branch...'); - const checkoutResult = execCommand('git checkout feature/model-tiers', { cwd: buildDir, silent: false }); - - if (!checkoutResult.success) { - printError('Failed to checkout feature/model-tiers branch'); - return false; - } - - // Install dependencies (monorepo workspace — must run from root) - printInfo('Installing monorepo dependencies with bun (this may take a minute)...'); - const installResult = execCommand('bun install', { cwd: buildDir, silent: false }); - - if (!installResult.success) { - printError('Failed to install dependencies'); - return false; - } - - // Build standalone binary using Bun's native compiler - printInfo('Building OpenCode standalone binary with Bun compiler...'); - const buildResult = execCommand( - 'bun run ./packages/opencode/script/build.ts --single', - { cwd: buildDir, silent: false } - ); - - if (!buildResult.success) { - printError('Build failed'); - printInfo('Make sure you have Bun 1.3.9+ installed: bun upgrade'); - return false; - } - - // Locate binary using deterministic platform-based path - // Bun.build() outputs to: dist/opencode-{os}-{arch}/bin/opencode - printInfo('Locating built binary...'); - const binaryPath = locateBuildBinary(buildDir); - - if (!binaryPath) { - printError(`Binary not found for platform ${process.platform}-${process.arch}`); - printInfo('Expected at: packages/opencode/dist/opencode-{os}-{arch}/bin/opencode'); - printInfo('Check build output above for errors.'); - return false; - } - - printSuccess(`Found binary: ${binaryPath.replace(buildDir + '/', '')}`); - - // Determine install location (local or system) - const localBin = join(HOME, '.local', 'bin'); - const usrLocalBin = '/usr/local/bin'; - let installPath: string; - - // Prefer ~/.local/bin — create it if it doesn't exist - if (!existsSync(localBin)) { - try { - mkdirSync(localBin, { recursive: true }); - } catch { - // Fall through to /usr/local/bin - } - } - - if (existsSync(localBin)) { - installPath = join(localBin, 'opencode'); - } else { - installPath = join(usrLocalBin, 'opencode'); - } - - // Copy binary to install location - printInfo(`Installing to ${installPath}...`); - const copyResult = execCommand( - `cp "${binaryPath}" "${installPath}"`, - { silent: true } - ); - - if (!copyResult.success) { - printWarning('May need sudo to install to system path'); - printInfo('Trying with sudo...'); - const sudoResult = execCommand( - `sudo cp "${binaryPath}" "${installPath}"`, - { silent: false } - ); - if (!sudoResult.success) { - printError(`Installation failed — you can manually copy the binary:`); - printInfo(` cp "${binaryPath}" ~/.local/bin/opencode`); - return false; - } - } - - // Make binary executable - try { - execSync(`chmod +x "${installPath}"`, { stdio: 'pipe' }); - } catch (e) { - printWarning('Failed to set executable permissions'); - } - - // Verify — use the FULL install path, not PATH-based lookup - // (user may not have ~/.local/bin in PATH yet) - const verifyResult = execCommand(`"${installPath}" --version`, { silent: true }); - if (verifyResult.success) { - printSuccess(`OpenCode ${verifyResult.output} installed to ${installPath}`); - } else { - printWarning('Binary was copied but version check failed'); - printInfo(`Tried: ${installPath} --version`); - printInfo('The binary may need different platform libraries or permissions.'); - } - - // Check if install directory is in PATH — if not, offer to fix it - const installDir = dirname(installPath); - const pathDirs = (process.env.PATH || '').split(':'); - const isInPath = pathDirs.some(p => { - try { return realpathSync(p) === realpathSync(installDir); } catch { return p === installDir; } - }); - - if (!isInPath) { - printWarning(`${installDir} is NOT in your PATH`); - printInfo('The "opencode" command won\'t be found until you add it.'); - print(''); - - // Detect shell config file - const shell = process.env.SHELL || '/bin/zsh'; - const shellName = shell.split('/').pop() || 'zsh'; - let shellConfig: string; - - if (shellName === 'zsh') { - shellConfig = join(HOME, '.zshrc'); - } else if (shellName === 'bash') { - // macOS uses .bash_profile for login shells, .bashrc for non-login - const bashProfile = join(HOME, '.bash_profile'); - shellConfig = existsSync(bashProfile) ? bashProfile : join(HOME, '.bashrc'); - } else if (shellName === 'fish') { - shellConfig = join(HOME, '.config', 'fish', 'config.fish'); - } else { - shellConfig = join(HOME, `.${shellName}rc`); - } - - const exportLine = shellName === 'fish' - ? `set -gx PATH "${installDir}" $PATH` - : `export PATH="${installDir}:$PATH"`; - - const addChoice = await promptChoice( - `Add ${installDir} to your PATH in ${shellConfig}?`, - [ - `Yes, add to ${shellConfig.replace(HOME, '~')} (Recommended)`, - 'No, I\'ll do it manually', - ], - 0 - ); - - if (addChoice === 0) { - try { - const existingContent = existsSync(shellConfig) - ? readFileSync(shellConfig, 'utf-8') - : ''; - - // Only add if not already present - if (!existingContent.includes(installDir)) { - const addition = `\n# Added by PAI-OpenCode Wizard\n${exportLine}\n`; - writeFileSync(shellConfig, existingContent + addition); - printSuccess(`Added PATH entry to ${shellConfig.replace(HOME, '~')}`); - printInfo(`Run: source ${shellConfig.replace(HOME, '~')} (or open a new terminal)`); - } else { - printSuccess(`${installDir} already referenced in ${shellConfig.replace(HOME, '~')}`); - } - } catch (err: any) { - printError(`Could not write to ${shellConfig}: ${err.message}`); - printInfo(`Add this line manually to ${shellConfig.replace(HOME, '~')}:`); - printInfo(` ${exportLine}`); - } - } else { - printInfo(`Add this line to ${shellConfig.replace(HOME, '~')}:`); - printInfo(` ${exportLine}`); - printInfo('Then restart your terminal or run: source ' + shellConfig.replace(HOME, '~')); - } - } else { - // PATH is fine — verify the command is actually findable via PATH too - const pathVerify = execCommand('opencode --version', { silent: true }); - if (!pathVerify.success && verifyResult.success) { - printWarning('Binary works directly but "opencode" command not found via PATH'); - printInfo('You may need to restart your terminal for PATH changes to take effect'); - } - } - - // Clean up - execCommand(`rm -rf ${buildDir}`, { silent: true }); - printSuccess('Cleaned up build directory'); - - return true; - } catch (err: any) { - printError(`Build error: ${err.message}`); - return false; - } -} - -/** - * Locate the built binary using deterministic platform-based path. - * Bun.build({ compile: true }) outputs to: dist/opencode-{os}-{arch}/bin/opencode - */ -function locateBuildBinary(buildDir: string): string | null { - const os = process.platform; // 'darwin', 'linux', 'win32' - const arch = process.arch; // 'arm64', 'x64' - - const osName = os === 'win32' ? 'windows' : os; - const binaryName = os === 'win32' ? 'opencode.exe' : 'opencode'; - - // Primary: exact platform match - const primaryPath = join(buildDir, 'packages', 'opencode', 'dist', `opencode-${osName}-${arch}`, 'bin', binaryName); - if (existsSync(primaryPath)) { - return primaryPath; - } - - // Fallback: try baseline variant (x64 without AVX2) - const baselinePath = join(buildDir, 'packages', 'opencode', 'dist', `opencode-${osName}-${arch}-baseline`, 'bin', binaryName); - if (existsSync(baselinePath)) { - return baselinePath; - } - - // Last resort: search dist directory for any matching binary - try { - const findResult = execCommand( - `find packages/opencode/dist -name "${binaryName}" -type f 2>/dev/null | head -1`, - { cwd: buildDir, silent: true } - ); - if (findResult.success && findResult.output) { - return join(buildDir, findResult.output); - } - } catch { - // Find failed, return null - } - - return null; -} - -// ============================================================================ -// STEP 2: CHOOSE SETUP PRESET -// ============================================================================ - -async function selectPreset(): Promise<(typeof PRESETS)[number]> { - print(''); - print(`${c.bold}Step 2: Choose Your Setup${c.reset}`); - print(`${c.gray}─────────────────────────────────────────────────${c.reset}`); - print(''); - print(' Choose how you want to power your AI agents:'); - print(''); - - const choices = PRESETS.map(p => { - const name = `${p.name}`; - const desc = p.description; - return `${c.bold}${name}${c.reset}\n ${c.gray}${desc}${c.reset}`; - }); - - const choice = await promptChoice('', choices, 0); - const selected = PRESETS[choice]; - - print(''); - printSuccess(`Selected: ${selected.name}`); - - if (selected.authNote) { - print(''); - print(` ${c.bold}Authentication:${c.reset}`); - print(` ${c.gray}${selected.authNote}${c.reset}`); - } - - print(''); - print(` ${c.gray}💡 Other providers (OpenAI, Google, etc.) can be configured later in opencode.json${c.reset}`); - - return selected; -} - -// ============================================================================ -// STEP 3: YOUR IDENTITY -// ============================================================================ - -async function setupIdentity(): Promise<{ name: string; timezone: string }> { - print(''); - print(`${c.bold}Step 3: Your Identity${c.reset}`); - print(`${c.gray}─────────────────────────────────────────────────${c.reset}`); - - const name = await prompt('What is your name?', 'User'); - const detectedTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone; - printInfo(`Detected timezone: ${detectedTimezone}`); - const timezone = await prompt('Timezone (press Enter to accept)', detectedTimezone); - - return { name, timezone }; -} - -// ============================================================================ -// STEP 4: YOUR AI ASSISTANT -// ============================================================================ - -async function setupAIAssistant(): Promise<{ name: string; catchphrase: string }> { - print(''); - print(`${c.bold}Step 4: Your AI Assistant${c.reset}`); - print(`${c.gray}─────────────────────────────────────────────────${c.reset}`); - - const name = await prompt('Name your AI assistant', 'PAI'); - const catchphrase = await prompt('Startup catchphrase', `${name} here, ready to help.`); - - return { name, catchphrase }; -} - -// ============================================================================ -// STEP 5: WRITE CONFIGURATION -// ============================================================================ - -// Profile structure from YAML -interface AgentTier { - quick?: string; - standard?: string; - advanced?: string; -} - -interface AgentConfig { - model: string; - tiers?: AgentTier; -} - -interface ProfileData { - name: string; - description: string; - default_model: string; - agents: Record; -} - -function loadProfile(profileName: string): ProfileData | null { - const profilePath = join(PROFILES_DIR, `${profileName}.yaml`); - - if (!existsSync(profilePath)) { - printError(`Profile not found: ${profilePath}`); - return null; - } - - try { - const content = readFileSync(profilePath, 'utf-8'); - return parseYaml(content) as ProfileData; - } catch (err: any) { - printError(`Failed to parse profile: ${err.message}`); - return null; - } -} - -function generateOpencodeJson(profile: ProfileData, username: string): object { - const agentBlock: Record = {}; - - // Convert profile agents to opencode.json format - for (const [agentName, agentConfig] of Object.entries(profile.agents)) { - const agentEntry: Record = { - model: agentConfig.model, - }; - - // Add model_tiers if present - if (agentConfig.tiers) { - agentEntry.model_tiers = {}; - for (const [tierName, tierModel] of Object.entries(agentConfig.tiers)) { - if (tierModel) { - agentEntry.model_tiers[tierName] = { model: tierModel }; - } - } - } - - agentBlock[agentName] = agentEntry; - } - - return { - $schema: 'https://opencode.ai/config.json', - theme: 'dark', - model: profile.default_model, - snapshot: true, - username: username, - agent: agentBlock, - }; -} - -function generateSettingsJson(config: PresetConfig): object { - return { - paiVersion: '2.5-opencode', - env: { - PAI_DIR: OPENCODE_DIR, - OPENCODE_MAX_OUTPUT_TOKENS: '80000', - BASH_DEFAULT_TIMEOUT_MS: '600000', - }, - contextFiles: [ - 'skills/PAI/SKILL.md', - 'skills/PAI/SYSTEM/AISTEERINGRULES.md', - 'skills/PAI/USER/AISTEERINGRULES.md', - 'skills/PAI/USER/DAIDENTITY.md', - ], - daidentity: { - name: config.AI_NAME, - fullName: `${config.AI_NAME} - Personal AI`, - displayName: config.AI_NAME, - color: '#3B82F6', - startupCatchphrase: config.CATCHPHRASE, - }, - principal: { - name: config.PRINCIPAL_NAME, - timezone: config.TIMEZONE, - }, - pai: { - source: 'github.com/Steffen025/pai-opencode', - upstream: 'github.com/danielmiessler/PAI', - version: '2.5', - }, - techStack: { - browser: 'arc', - terminal: 'terminal', - packageManager: 'bun', - language: 'TypeScript', - }, - provider: { - id: config.PRESET.id, - name: config.PRESET.name, - profile: config.PRESET.profile, - }, - }; -} - -function generateDAIdentity(config: PresetConfig): string { - return `# DA Identity & Interaction Rules - -**Personal content - DO NOT commit to public repositories.** - ---- - -## My Identity - -- **Full Name:** ${config.AI_NAME} - Personal AI -- **Name:** ${config.AI_NAME} -- **Display Name:** ${config.AI_NAME} -- **Color:** #3B82F6 (Tailwind Blue-500) -- **Role:** Your AI assistant -- **Operating Environment:** Personal AI infrastructure built on OpenCode - ---- - -## First-Person Voice (CRITICAL) - -The DA should speak as itself, not about itself in third person. - -| Do This | Not This | -|---------|----------| -| "for my system" / "in my architecture" | "for ${config.AI_NAME}" / "for the ${config.AI_NAME} system" | -| "I can help" / "my approach" | "${config.AI_NAME} can help" | -| "we built this together" | "the system can" | - ---- - -## Your Information - -- **Name:** ${config.PRINCIPAL_NAME} -- **Timezone:** ${config.TIMEZONE} -- **Provider:** ${config.PRESET.name} - ---- - -## Personality & Behavior - -- **Friendly and professional** - Approachable but competent -- **Adaptive** - Adjusts communication style based on context -- **Honest** - Committed to truthful communication -- **First-person voice** - Always "I" and "me", never third person - ---- - -## Pronoun Convention - -- Refer to ${config.PRINCIPAL_NAME} as **"you"** (second person) -- Refer to itself as **"I"** or **"me"** (first person) -- **NEVER** use "the user", "the principal", or generic terms - ---- - -*Generated by PAI-OpenCode Wizard on ${new Date().toISOString().split('T')[0]}* -*Customize this file to define your AI's personality* -`; -} - -function generateBasicInfo(config: PresetConfig): string { - return `# Basic Information - -- **Name:** ${config.PRINCIPAL_NAME} -- **Timezone:** ${config.TIMEZONE} -- **AI Provider:** ${config.PRESET.name} -- **AI Profile:** ${config.PRESET.profile} - ---- - -*Generated by PAI-OpenCode Wizard* -*Update this file with your personal details* -`; -} - -function generateInternContextNote(): string { - return `# InternContext.md - -**NOTE:** This file is intentionally minimal. - -The Intern agent uses context files from other agents to understand PAI system behavior: -- Agent personality definitions from PAI/Components/ -- Agent routing rules from PAI/SYSTEM/PAIAGENTSYSTEM.md -- Core PAI principles from PAI/SKILL.md - -This file exists to satisfy the InternContext.md reference but contains no additional content, as the Intern agent's context is derived from shared PAI system files. - ---- - -*Generated by PAI-OpenCode Wizard* -`; -} - -async function writeConfiguration(config: PresetConfig): Promise { - print(''); - print(`${c.bold}Step 5: Writing Configuration${c.reset}`); - print(`${c.gray}─────────────────────────────────────────────────${c.reset}`); - - // Load profile - const profile = loadProfile(config.PRESET.profile); - if (!profile) { - return false; - } - - // Generate and write opencode.json - const opencodeJson = generateOpencodeJson(profile, config.PRINCIPAL_NAME); - writeFileSync(join(PROJECT_ROOT, 'opencode.json'), JSON.stringify(opencodeJson, null, 2)); - printSuccess('Created opencode.json (project root)'); - - // Generate and write settings.json - const settingsJson = generateSettingsJson(config); - writeFileSync(join(OPENCODE_DIR, 'settings.json'), JSON.stringify(settingsJson, null, 2)); - printSuccess('Created settings.json (.opencode/)'); - - // Ensure PAI/USER directory exists - const userDir = join(OPENCODE_DIR, 'skills', 'PAI', 'USER'); - if (!existsSync(userDir)) { - mkdirSync(userDir, { recursive: true }); - } - - // Generate and write DAIDENTITY.md - const daIdentityPath = join(userDir, 'DAIDENTITY.md'); - writeFileSync(daIdentityPath, generateDAIdentity(config)); - printSuccess('Created DAIDENTITY.md'); - - // Generate and write BASICINFO.md - const basicInfoPath = join(userDir, 'BASICINFO.md'); - writeFileSync(basicInfoPath, generateBasicInfo(config)); - printSuccess('Created BASICINFO.md'); - - // Generate InternContext.md note - const internContextPath = join(userDir, 'InternContext.md'); - writeFileSync(internContextPath, generateInternContextNote()); - printSuccess('Created InternContext.md'); - - // Create required directories - const requiredDirs = [ - 'MEMORY', - 'MEMORY/STATE', - 'MEMORY/LEARNING', - 'MEMORY/WORK', - 'MEMORY/LEARNING/SIGNALS', - ]; - for (const dir of requiredDirs) { - const dirPath = join(OPENCODE_DIR, dir); - if (!existsSync(dirPath)) { - mkdirSync(dirPath, { recursive: true }); - } - } - printSuccess('Created MEMORY directories'); - - // Fix permissions - fixPermissions(OPENCODE_DIR); - - return true; -} - -// ============================================================================ -// PERMISSIONS -// ============================================================================ - -function fixPermissions(targetDir: string): void { - const info = userInfo(); - - print(''); - print(`${c.bold}Fixing permissions for ${info.username}${c.reset}`); - print(`${c.gray}─────────────────────────────────────────────────${c.reset}`); - - try { - execSync(`chmod -R 755 "${targetDir}"`, { stdio: 'pipe' }); - printSuccess('chmod -R 755 (make accessible)'); - - execSync(`chown -R ${info.uid}:${info.gid} "${targetDir}"`, { stdio: 'pipe' }); - printSuccess(`chown -R to ${info.username}`); - - // Make scripts executable - for (const pattern of ['*.ts', '*.sh']) { - try { - execSync(`find "${targetDir}" -name "${pattern}" -exec chmod 755 {} \\;`, { stdio: 'pipe' }); - } catch (e) { /* ignore */ } - } - printSuccess('Set executable permissions on scripts'); - } catch (err: any) { - printWarning(`Permission fix may need sudo: ${err.message}`); - } -} - -// ============================================================================ -// STEP 5b: SYMLINK SETUP -// ============================================================================ - -async function setupSymlink(): Promise { - print(''); - print(`${c.bold}Setting up ~/.opencode symlink${c.reset}`); - print(`${c.gray}─────────────────────────────────────────────────${c.reset}`); - - const homeOpencode = join(HOME, '.opencode'); - - // Case 1: Nothing exists → create symlink - if (!existsSync(homeOpencode)) { - try { - symlinkSync(OPENCODE_DIR, homeOpencode); - printSuccess(`Created symlink: ~/.opencode → ${OPENCODE_DIR}`); - } catch (err: any) { - printWarning(`Could not create symlink: ${err.message}`); - printInfo(`Create it manually: ln -s ${OPENCODE_DIR} ~/.opencode`); - } - return; - } - - // Case 2: Symlink already exists - try { - const stats = lstatSync(homeOpencode); - if (stats.isSymbolicLink()) { - printInfo(`Symlink ~/.opencode already exists`); - - // Check if it points to the right place - const resolvedTarget = realpathSync(homeOpencode); - const resolvedOpencode = realpathSync(OPENCODE_DIR); - if (resolvedTarget === resolvedOpencode) { - printSuccess('Symlink points to this installation ✓'); - } else { - printWarning(`Symlink points to: ${resolvedTarget}`); - printInfo(`Expected: ${resolvedOpencode}`); - - const choice = await promptChoice('Update symlink to point to this installation?', [ - 'Yes, update symlink (Recommended)', - 'No, keep existing symlink', - ], 0); - - if (choice === 0) { - try { - unlinkSync(homeOpencode); - symlinkSync(OPENCODE_DIR, homeOpencode); - printSuccess(`Updated symlink: ~/.opencode → ${OPENCODE_DIR}`); - } catch (err: any) { - printError(`Failed to update symlink: ${err.message}`); - } - } - } - return; - } - } catch (e) { - // lstatSync failed, treat as regular directory - } - - // Case 3: Regular directory exists (not a symlink) - printWarning('~/.opencode exists as a regular directory'); - print(''); - - const choice = await promptChoice('How would you like to handle this?', [ - 'Back up existing and create symlink (Recommended)', - 'Skip — I\'ll handle it manually', - ], 0); - - if (choice === 0) { - const backupPath = `${homeOpencode}.backup-${Date.now()}`; - try { - execCommand(`mv "${homeOpencode}" "${backupPath}"`, { silent: true }); - printSuccess(`Backed up to: ${backupPath}`); - - symlinkSync(OPENCODE_DIR, homeOpencode); - printSuccess(`Created symlink: ~/.opencode → ${OPENCODE_DIR}`); - } catch (err: any) { - printError(`Failed: ${err.message}`); - printInfo(`Manual steps:`); - printInfo(` mv ~/.opencode ~/.opencode.backup`); - printInfo(` ln -s ${OPENCODE_DIR} ~/.opencode`); - } - } else { - printInfo('Skipped symlink setup'); - printInfo(`To create manually: ln -s ${OPENCODE_DIR} ~/.opencode`); - } -} - -function setupConfigSymlink(): void { - const configSource = join(PROJECT_ROOT, 'opencode.json'); - const opencodeDir = join(PROJECT_ROOT, '.opencode'); - const symlinkTarget = join(opencodeDir, 'opencode.json'); - - // Only create if source exists and .opencode dir exists - if (!existsSync(configSource)) return; - if (!existsSync(opencodeDir)) return; - - // Check if symlink already exists and points to the right place - if (existsSync(symlinkTarget)) { - try { - const stats = lstatSync(symlinkTarget); - if (stats.isSymbolicLink()) { - const resolvedPath = realpathSync(symlinkTarget); - if (resolvedPath === realpathSync(configSource)) { - // Already correct - return; - } - // Wrong target, remove and recreate - unlinkSync(symlinkTarget); - } else { - // It's a real file, back it up and replace - const backup = symlinkTarget + '.bak'; - renameSync(symlinkTarget, backup); - printWarning(`Backed up existing .opencode/opencode.json to opencode.json.bak`); - } - } catch (e) { - // Can't read, try to remove - try { unlinkSync(symlinkTarget); } catch {} - } - } - - // Create relative symlink: .opencode/opencode.json → ../opencode.json - try { - symlinkSync('../opencode.json', symlinkTarget); - printSuccess('Created symlink: .opencode/opencode.json → ../opencode.json'); - } catch (e: any) { - printWarning(`Could not create config symlink: ${e.message}`); - } -} - -// ============================================================================ -// VALIDATION -// ============================================================================ - -function validate(): { passed: boolean; results: string[] } { - const results: string[] = []; - let passed = true; - - // Check opencode.json - const opencodeJsonPath = join(PROJECT_ROOT, 'opencode.json'); - if (existsSync(opencodeJsonPath)) { - try { - const config = JSON.parse(readFileSync(opencodeJsonPath, 'utf-8')); - if (config.model) { - results.push(`${c.green}✓${c.reset} opencode.json valid (model: ${config.model})`); - } else { - results.push(`${c.red}✗${c.reset} opencode.json missing model field`); - passed = false; - } - if (config.agent) { - const agentCount = Object.keys(config.agent).length; - results.push(`${c.green}✓${c.reset} Agent routing configured (${agentCount} agents)`); - } - } catch (e) { - results.push(`${c.red}✗${c.reset} opencode.json invalid JSON`); - passed = false; - } - } else { - results.push(`${c.red}✗${c.reset} opencode.json not found`); - passed = false; - } - - // Check settings.json - const settingsPath = join(OPENCODE_DIR, 'settings.json'); - if (existsSync(settingsPath)) { - try { - const settings = JSON.parse(readFileSync(settingsPath, 'utf-8')); - if (settings.principal?.name && settings.daidentity?.name && settings.provider?.id) { - results.push(`${c.green}✓${c.reset} settings.json valid (provider: ${settings.provider.id})`); - } else { - results.push(`${c.red}✗${c.reset} settings.json missing required fields`); - passed = false; - } - } catch (e) { - results.push(`${c.red}✗${c.reset} settings.json invalid JSON`); - passed = false; - } - } else { - results.push(`${c.red}✗${c.reset} settings.json not found`); - passed = false; - } - - // Check directories - for (const dir of ['skills', 'MEMORY', 'plugins']) { - if (existsSync(join(OPENCODE_DIR, dir))) { - results.push(`${c.green}✓${c.reset} ${dir}/ exists`); - } else { - results.push(`${c.yellow}!${c.reset} ${dir}/ missing (will be created)`); - } - } - - // Check PAI skill - if (existsSync(join(OPENCODE_DIR, 'skills', 'PAI', 'SKILL.md'))) { - results.push(`${c.green}✓${c.reset} PAI skill found`); - } else { - results.push(`${c.red}✗${c.reset} PAI skill missing`); - passed = false; - } - - return { passed, results }; -} - -// ============================================================================ -// MAIN -// ============================================================================ - -async function main(): Promise { - // Show help if requested - if (process.argv.includes('--help') || process.argv.includes('-h')) { - print(''); - print(`${c.blue}${c.bold}PAI-OpenCode Installation Wizard v2.0${c.reset}`); - print(''); - print(' Interactive setup for Personal AI Infrastructure'); - print(''); - print(`${c.bold}Usage:${c.reset}`); - print(' bun run PAIOpenCodeWizard.ts Run the interactive wizard'); - print(' bun run PAIOpenCodeWizard.ts --help Show this help message'); - print(''); - print(`${c.bold}What it does:${c.reset}`); - print(' 1. Checks prerequisites (Bun 1.3.9+, git)'); - print(' 2. Builds OpenCode from development source (Bun native compiler)'); - print(' 3. Guides you through 4 preset provider choices'); - print(' 4. Sets up your identity and AI assistant name'); - print(' 5. Generates configuration files with model_tiers support'); - print(''); - print(`${c.bold}Presets:${c.reset}`); - print(' A. Anthropic — Best quality, API key or subscription'); - print(' B. ZEN PAID — Budget-friendly, privacy-preserving'); - print(' C. ZEN FREE — No cost, data may be used for improvement'); - print(' D. Ollama — Fully offline, local models'); - print(''); - process.exit(0); - } - - // Banner - print(''); - print(`${c.blue}${c.bold}┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓${c.reset}`); - print(`${c.blue}${c.bold}┃${c.reset} ${c.cyan}PAI-OpenCode Installation Wizard v2.0${c.reset} ${c.blue}${c.bold}┃${c.reset}`); - print(`${c.blue}${c.bold}┃${c.reset} ${c.gray}Personal AI Infrastructure for OpenCode${c.reset} ${c.blue}${c.bold}┃${c.reset}`); - print(`${c.blue}${c.bold}┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛${c.reset}`); - - // Step 0: Prerequisites - const prereqs = checkPrerequisites(); - - if (!prereqs.bun || !prereqs.git) { - print(''); - printError('Missing required prerequisites'); - printInfo('Please install the missing tools and run the wizard again.'); - process.exit(1); - } - - // Step 1: Build OpenCode - const buildSuccess = await buildOpenCode(); - if (!buildSuccess) { - printWarning('OpenCode build had issues — you can still continue but may encounter errors'); - } - - // Step 2: Select preset - const preset = await selectPreset(); - - // Step 3: Identity - const { name, timezone } = await setupIdentity(); - - // Step 4: AI Assistant - const { name: aiName, catchphrase } = await setupAIAssistant(); - - // Step 5: Write configuration - const config: PresetConfig = { - PRINCIPAL_NAME: name, - TIMEZONE: timezone, - AI_NAME: aiName, - CATCHPHRASE: catchphrase, - PRESET: preset, - }; - - const writeSuccess = await writeConfiguration(config); - if (!writeSuccess) { - print(''); - printError('Configuration failed'); - process.exit(1); - } - - // Install project dependencies (yaml, diff, etc.) - print(''); - print(`${c.bold}Installing Dependencies${c.reset}`); - print(`${c.gray}─────────────────────────────────────────────────${c.reset}`); - const depInstall = execCommand('bun install', { cwd: PROJECT_ROOT, silent: false }); - if (depInstall.success) { - printSuccess('Dependencies installed'); - } else { - printWarning('Dependency install failed — run "bun install" manually in the project root'); - } - - // Create config symlink: .opencode/opencode.json → ../opencode.json - setupConfigSymlink(); - - // Step 5b: Symlink setup - await setupSymlink(); - - // Step 6: Validate and show success - print(''); - const { passed, results } = validate(); - print(`${c.bold}Validation${c.reset}`); - print(`${c.gray}─────────────────────────────────────────────────${c.reset}`); - for (const r of results) print(` ${r}`); - - print(''); - if (passed) { - print(`${c.green}${c.bold}┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓${c.reset}`); - print(`${c.green}${c.bold}┃${c.reset} ${c.green}✓ PAI-OpenCode Installed!${c.reset} ${c.green}${c.bold}┃${c.reset}`); - print(`${c.green}${c.bold}┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛${c.reset}`); - print(''); - print(` ${c.cyan}Your AI:${c.reset} ${aiName}`); - print(` ${c.cyan}Principal:${c.reset} ${name}`); - print(` ${c.cyan}Provider:${c.reset} ${preset.name}`); - print(` ${c.cyan}Profile:${c.reset} ${preset.profile}`); - print(''); - print(`${c.bold}Next Steps:${c.reset}`); - print(''); - print(` ${c.cyan}1.${c.reset} Start OpenCode in this directory:`); - print(` ${c.green}opencode${c.reset}`); - print(''); - - if (preset.authType === 'oauth') { - print(` ${c.cyan}2.${c.reset} Authenticate with your provider:`); - print(` ${c.green}/connect${c.reset}`); - print(''); - } else if (preset.authType === 'apikey-or-oauth') { - print(` ${c.cyan}2.${c.reset} Set up authentication (choose one):`); - print(` ${c.gray}Option A:${c.reset} Add ${c.green}ANTHROPIC_API_KEY=sk-...${c.reset} to ${c.cyan}.opencode/.env${c.reset}`); - print(` ${c.gray}Option B:${c.reset} Run ${c.green}/connect${c.reset} in OpenCode to use your Anthropic subscription`); - print(''); - } - - print(` ${c.cyan}3.${c.reset} ${c.bold}Deep Personalization (Recommended):${c.reset}`); - print(` Once OpenCode starts, run the onboarding wizard:`); - print(''); - print(` ${c.gray}┌──────────────────────────────────────────────────────────┐${c.reset}`); - print(` ${c.gray}│${c.reset} Let's do the onboarding. Guide me through setting up my ${c.gray}│${c.reset}`); - print(` ${c.gray}│${c.reset} personal context — my goals, values, and how I want you ${c.gray}│${c.reset}`); - print(` ${c.gray}│${c.reset} to work with me. ${c.gray}│${c.reset}`); - print(` ${c.gray}└──────────────────────────────────────────────────────────┘${c.reset}`); - print(''); - print(` ${c.cyan}4.${c.reset} ${c.bold}Switch Providers Anytime:${c.reset}`); - print(` Want to use OpenAI, Google, or a different provider later?`); - print(''); - print(` ${c.green}bun run .opencode/tools/switch-provider.ts --list${c.reset} ${c.gray}# See all profiles${c.reset}`); - print(` ${c.green}bun run .opencode/tools/switch-provider.ts openai${c.reset} ${c.gray}# Switch to GPT${c.reset}`); - print(''); - print(` For advanced configuration, see:`); - print(` ${c.cyan}docs/ADVANCED-SETUP.md${c.reset}`); - print(''); - } else { - print(`${c.red}${c.bold}✗ Installation has issues${c.reset}`); - print(` Check the validation results above.`); - process.exit(1); - } - - process.exit(0); -} - -// Only execute if this is the main script (not imported) -const isMainScript = typeof Bun !== 'undefined' && Bun.main === import.meta.path; -if (isMainScript) { - main().catch(err => { - console.error(`${c.red}Error:${c.reset}`, err.message); - process.exit(1); - }); -} diff --git a/.opencode/tsconfig.json b/.opencode/tsconfig.json index a2f77d50..6fcb1687 100644 --- a/.opencode/tsconfig.json +++ b/.opencode/tsconfig.json @@ -17,5 +17,5 @@ } }, "include": ["**/*.ts"], - "exclude": ["node_modules", "_deprecated", "PAIOpenCodeWizard.ts", "observability-server", "voice-server", "skills/**/Tools"] + "exclude": ["node_modules", "_deprecated", "observability-server", "voice-server", "skills/**/Tools"] } diff --git a/.prd/PRD-20260327-install-docs-cleanup.md b/.prd/PRD-20260327-install-docs-cleanup.md new file mode 100644 index 00000000..3d62ddff --- /dev/null +++ b/.prd/PRD-20260327-install-docs-cleanup.md @@ -0,0 +1,77 @@ +--- +prd: true +id: PRD-20260327-install-docs-cleanup +status: COMPLETE +mode: interactive +effort_level: Extended +created: 2026-03-27 +updated: 2026-03-27 +iteration: 0 +maxIterations: 128 +loopStatus: null +last_phase: VERIFY +failing_criteria: [] +verification_summary: "12/12" +parent: null +children: [] +--- + +# pai-opencode install + docs cleanup (v3) + +Delete retired install artifacts (wizard + GUI), make CLI installer canonical, and update all docs accordingly. + +## STATUS + +| What | State | +|---|---| +| Progress | 12/12 criteria passing | +| Phase | COMPLETE | +| Next action | None | +| Blocked by | Nothing | + +## CONTEXT + +### Problem Space +The repo currently documents multiple install paths (wizard vs PAI-Install) and includes GUI components that are not desired. The goal is a single CLI-only install flow and docs that match reality. + +### Key Files +- `PAI-Install/install.sh` - Installer bootstrap entrypoint +- `PAI-Install/cli/quick-install.ts` - Headless installer implementation +- `README.md` - Project overview + quick start +- `INSTALL.md` - Canonical installation guide +- `docs/` - Supporting documentation + +### Constraints +- Remove GUI installer (Electron/web/public). +- Remove `.opencode/PAIOpenCodeWizard.ts` wizard. +- Documentation must not reference removed entrypoints. + +## PLAN + +- Make `PAI-Install/install.sh` CLI-only (no GUI path). +- Delete GUI directories and broken GUI entrypoint (`PAI-Install/main.ts`). +- Delete wizard (`.opencode/PAIOpenCodeWizard.ts`). +- Update `README.md`, `INSTALL.md`, and `docs/` to reference `PAI-Install/install.sh --cli`. +- Verify via grep checks + running installer help paths. + +## IDEAL STATE CRITERIA (Verification Criteria) + +- [x] ISC-C1: GUI installer directories are removed from PAI-Install | Verify: CLI: test ! -d PAI-Install/electron +- [x] ISC-C2: Web/GUI assets are removed from PAI-Install | Verify: CLI: test ! -d PAI-Install/web +- [x] ISC-C3: PAI-Install entrypoint no longer references electron | Verify: Grep: "electron" has 0 matches in PAI-Install/install.sh +- [x] ISC-C4: Wizard file is removed from .opencode | Verify: CLI: test ! -f .opencode/PAIOpenCodeWizard.ts +- [x] ISC-C5: README quick start uses PAI-Install/install.sh CLI flow | Verify: Read: README.md install commands updated +- [x] ISC-C6: INSTALL.md uses only PAI-Install/install.sh CLI flow | Verify: Read: INSTALL.md contains no wizard invocation +- [x] ISC-C7: docs/ contain no references to removed wizard path | Verify: Grep: PAIOpenCodeWizard has 0 matches in docs/ +- [x] ISC-C8: docs/ contain no instructions to run removed GUI mode | Verify: Grep: "--mode gui" has 0 matches in docs/ +- [x] ISC-C9: PAI-Install/README.md matches actual directory structure | Verify: Read: PAI-Install/README.md list is accurate +- [x] ISC-C10: CLI installer help runs without throwing | Verify: CLI: bun PAI-Install/cli/quick-install.ts --help +- [x] ISC-C11: install.sh CLI mode works and forwards args | Verify: CLI: bash PAI-Install/install.sh --cli --help +- [x] ISC-A1: Docs never recommend bun run .opencode/PAIOpenCodeWizard.ts | Verify: Grep: pattern has 0 matches + +## LOG + +### Iteration 0 — 2026-03-27 +- Phase reached: VERIFY +- Criteria progress: 12/12 +- Work done: CLI-only installer enforced, GUI/wizard artifacts deleted, docs updated repo-wide diff --git a/ANNOUNCEMENT-v2.0.0.md b/ANNOUNCEMENT-v2.0.0.md index d0b020ea..bb7f7331 100644 --- a/ANNOUNCEMENT-v2.0.0.md +++ b/ANNOUNCEMENT-v2.0.0.md @@ -45,11 +45,11 @@ Pay exactly what the task requires. git clone https://github.com/Steffen025/pai-opencode cd pai-opencode bun install -bun run .opencode/PAIOpenCodeWizard.ts +bash PAI-Install/install.sh --cli --preset zen --name "Your Name" --ai-name "Jeremy" opencode ``` -Choose a provider preset (`zen-paid`, `openrouter`, `local-ollama`), configure your API keys, and you're running. +Choose a provider preset (`zen`, `openrouter`, `openai`, `anthropic`), configure your API keys, and you're running. ## What Didn't Change diff --git a/CHANGELOG.md b/CHANGELOG.md index 38044b41..e2c8d425 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -148,31 +148,31 @@ Porting 14 upstream commits spanning Algorithm v1.3.0 through v1.8.0: --- -## v2.0.1 — Wizard Build Process Fix (2026-02-20) +## v2.0.1 — Legacy Installer Build Process Fix (2026-02-20) ### Fixed -#### Critical: Wizard Build-from-Source Completely Broken -- **Symptom:** `git clone` succeeds but wizard reports "Failed to clone repository" and aborts +#### Critical: Legacy Installer Build-from-Source Completely Broken +- **Symptom:** `git clone` succeeds but legacy installer reports "Failed to clone repository" and aborts - **Root cause:** `execCommand()` with `stdio: 'inherit'` returns `null` from `execSync()`. Calling `.trim()` on `null` throws TypeError, caught as failure. **Every non-silent command falsely reports failure.** - **Fix:** Null-safe guard: `output.trim()` → `(output ?? '').trim()` #### Go Prerequisite Removed (No Longer Needed) - **Context:** OpenCode was historically a Go project (BubbleTea TUI). It has been completely rewritten to Bun/TypeScript. The build now uses `Bun.build({ compile: true })` to produce native binaries — no Go toolchain needed. -- **Fix:** Removed Go prerequisite check from wizard and all documentation +- **Fix:** Removed Go prerequisite check from the legacy installer and all documentation - **Fix:** Strengthened Bun version check from 1.3+ to 1.3.9+ (matches OpenCode monorepo `packageManager` field) #### Binary Detection After Build -- **Symptom:** After successful build, wizard couldn't find the binary +- **Symptom:** After successful build, legacy installer couldn't find the binary - **Root cause:** Generic filename search didn't match `Bun.build()` output structure (`dist/opencode-{os}-{arch}/bin/opencode`) - **Fix:** Deterministic platform-based binary lookup using `process.platform` + `process.arch` with baseline fallback ### Changed -- **Wizard messaging** updated to reflect Bun-based build (not Go) +- **Legacy installer messaging** updated to reflect Bun-based build (not Go) - **INSTALL.md** — Prerequisites: removed Go, added Bun 1.3.9+ note. Manual install: complete rewrite with `bun run ./packages/opencode/script/build.ts --single`. WSL section: removed `golang-go` package. -- **README.md** — Quick Start: added prerequisites line, updated wizard step description -- **docs/MIGRATION.md** — Replaced `go install` with wizard command, replaced `$(go env GOPATH)/bin` with `~/.local/bin` in PATH troubleshooting -- **EXPLORATION-SUMMARY.md** — Updated wizard flow description +- **README.md** — Quick Start: added prerequisites line, updated installer step description +- **docs/MIGRATION.md** — Replaced `go install` with installer command, replaced `$(go env GOPATH)/bin` with `~/.local/bin` in PATH troubleshooting +- **EXPLORATION-SUMMARY.md (since removed)** — Updated wizard flow description --- @@ -328,7 +328,7 @@ agents: - **3 presets** ready to use ### Migration Guide -1. Re-run the wizard: `bun run .opencode/PAIOpenCodeWizard.ts` +1. Re-run the installer: `bash PAI-Install/install.sh --cli --help` 2. Or switch profile manually: `bun run .opencode/tools/switch-provider.ts zen-paid` 3. Custom agent models → Edit `opencode.json` agent section directly @@ -541,7 +541,7 @@ This release brings full PAI 2.5 Algorithm compatibility and adds 5 new handlers | `agent-capture.ts` | Captures agent outputs | #### Installation -- `PAIOpenCodeWizard.ts` - Interactive setup wizard +- Legacy interactive installer (removed; CLI installer is canonical) - 8 AI providers supported (Anthropic, OpenAI, Google, Groq, AWS Bedrock, Azure, ZEN, Ollama) - TELOS personalization framework @@ -582,7 +582,7 @@ This release brings full PAI 2.5 Algorithm compatibility and adds 5 new handlers | Verify Completion Gate | No | No | No | No | No | **Yes** | Yes | | Effort-Scaled Gates | No | No | No | No | No | **Yes** | Yes | | **DB Health Tooling** | No | No | No | No | No | No | **Yes** | -| **Electron GUI Installer** | No | No | No | No | No | No | **Yes** | +| **Installer (CLI-only)** | No | No | No | No | No | No | **Yes** | | **v2→v3 Migration** | No | No | No | No | No | No | **Yes** | | **Security Hardening** | No | No | No | No | No | No | **Full** | @@ -596,7 +596,7 @@ This release brings full PAI 2.5 Algorithm compatibility and adds 5 new handlers - Skills structure: flat → hierarchical (Category/Skill) - Config: single-file → dual-file (opencode.json + settings.json) - Paths: `.claude/` → `.opencode/` -- New Electron GUI installer +- New CLI-only installer **Recommended upgrade process:** @@ -615,9 +615,9 @@ This release brings full PAI 2.5 Algorithm compatibility and adds 5 new handlers bun Tools/migration-v2-to-v3.ts ``` -4. **Alternative: Fresh install with the new GUI:** +4. **Alternative: Fresh install (recommended for a clean setup):** ```bash - bash PAI-Install/install.sh + bash PAI-Install/install.sh --cli --help ``` **See [UPGRADE.md](/UPGRADE.md) for detailed step-by-step instructions.** @@ -628,10 +628,10 @@ This release brings full PAI 2.5 Algorithm compatibility and adds 5 new handlers git fetch origin git checkout main git pull origin main -bun run .opencode/PAIOpenCodeWizard.ts +bash PAI-Install/install.sh --cli --help ``` -Re-running the wizard is recommended — it generates the new profile format with dynamic tier routing. +Re-running the installer is recommended — it generates the new profile format with dynamic tier routing. **Manual alternative:** `bun run .opencode/tools/switch-provider.ts zen-paid` diff --git a/INSTALL.md b/INSTALL.md index 66bb7e8a..ee08c9c4 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -1,6 +1,6 @@ # Installation Guide -This guide will help you install PAI-OpenCode in under 5 minutes. +This guide will help you install PAI OpenCode in under 5 minutes. ![Installation Flow](docs/images/installation-flow.jpg) @@ -8,7 +8,7 @@ This guide will help you install PAI-OpenCode in under 5 minutes. ## Prerequisites -Before running the wizard, ensure you have: +Before running the installer, ensure you have: 1. **Git** — For cloning the repository - macOS: `brew install git` @@ -30,14 +30,17 @@ Before running the wizard, ensure you have: git clone https://github.com/Steffen025/pai-opencode.git cd pai-opencode -# 2. Run the Installation Wizard -bun run .opencode/PAIOpenCodeWizard.ts +# 2. Run the CLI installer +bash PAI-Install/install.sh --cli --help + +# Example: fresh install (Zen) +bash PAI-Install/install.sh --cli --preset zen --name "Your Name" --ai-name "Jeremy" # 3. Start OpenCode opencode ``` -The wizard will: +The installer will: 1. ✅ Check prerequisites (git, bun 1.3.9+) 2. ✅ **Build OpenCode from dev source** using Bun's native compiler (required for model tiers feature) 3. ✅ Ask you to choose a preset: @@ -68,8 +71,8 @@ mv ~/.opencode ~/.opencode.backup # 3. Symlink PAI-OpenCode to your home directory ln -s $(pwd)/.opencode ~/.opencode -# 4. Run the wizard (copies nothing — works in place) -bun run .opencode/PAIOpenCodeWizard.ts +# 4. Run the CLI installer (works in place) +bash PAI-Install/install.sh --cli --preset zen --name "Your Name" --ai-name "Jeremy" # 5. Start OpenCode opencode @@ -134,8 +137,8 @@ cd ~ git clone https://github.com/Steffen025/pai-opencode.git cd pai-opencode -# Run the Installation Wizard -bun run .opencode/PAIOpenCodeWizard.ts +# Run the CLI installer +bash PAI-Install/install.sh --cli --preset zen --name "Your Name" --ai-name "Jeremy" # Start OpenCode opencode @@ -174,7 +177,7 @@ After installation, see [ADVANCED-SETUP.md](docs/ADVANCED-SETUP.md) for: ## Manual Installation -If you prefer to install manually without the wizard: +If you prefer to run the headless installer directly: **Step 1:** Build OpenCode from dev source (required for model tiers) ```bash @@ -263,7 +266,7 @@ After installation, verify everything works: ### "Command not found: opencode" -The wizard installs OpenCode to `~/.local/bin/opencode` (preferred) or `/usr/local/bin/opencode` (fallback). +The installer builds/installs OpenCode depending on your selected preset and platform. **Check where it was installed:** ```bash @@ -325,11 +328,11 @@ This means you have the **stable OpenCode installed** instead of the PAI-OpenCod # Remove the stable version rm $(which opencode) -# Re-run the wizard which builds from our fork using Bun's native compiler -bun run .opencode/PAIOpenCodeWizard.ts +# Re-run the installer which builds from our fork using Bun's native compiler +bash PAI-Install/install.sh --cli --preset anthropic --name "Your Name" --ai-name "Jeremy" ``` -> **Note:** The wizard builds OpenCode from our fork (`Steffen025/opencode`, branch `feature/model-tiers`) using `Bun.build({ compile: true })`. This produces a standalone native binary — no Go, no runtime dependencies. +> **Note:** The installer can build OpenCode from our fork (`Steffen025/opencode`, branch `feature/model-tiers`) using `Bun.build({ compile: true })`. This produces a standalone native binary — no Go, no runtime dependencies. --- @@ -366,8 +369,8 @@ PAI-OpenCode uses a **preset system** for simplicity: ### Switching Presets ```bash -# Re-run the wizard to change preset -bun run .opencode/PAIOpenCodeWizard.ts +# Re-run the installer to change preset +bash PAI-Install/install.sh --cli --preset openai --name "Your Name" --ai-name "Jeremy" ``` ### Advanced Provider Setup diff --git a/PAI-Install/README.md b/PAI-Install/README.md index 0998a2ed..579b1787 100644 --- a/PAI-Install/README.md +++ b/PAI-Install/README.md @@ -1,71 +1,51 @@ -# PAI-OpenCode Installer +# PAI OpenCode Installer -> GUI and CLI installer for PAI-OpenCode v3.0 +CLI-only installer for PAI OpenCode. ## Quick Start ```bash -# Run the installer -bash PAI-Install/install.sh +bash PAI-Install/install.sh --cli --help + +# Example: fresh install (Zen) +bash PAI-Install/install.sh --cli --preset zen --name "Your Name" --ai-name "Jeremy" ``` ## What This Installer Does -1. **Detects** your environment (macOS/Linux) -2. **Installs** Bun runtime if not present -3. **Creates** `~/.opencode/` directory structure -4. **Copies** PAI core files (skills, plugins, handlers) -5. **Configures** `opencode.json` with Model Tiers -6. **Sets up** the Electron GUI (optional) +1. Checks prerequisites (git, bun) +2. Optionally builds a custom OpenCode binary (model tiers) +3. Generates/updates `opencode.json` and `~/.opencode/*` +4. Installs PAI files into `~/.opencode/` ## Directory Structure ``` PAI-Install/ -├── install.sh # Main bootstrap script -├── main.ts # TypeScript entry point -├── generate-welcome.ts # Welcome screen generator -├── cli/ # CLI installer module -│ ├── index.ts -│ ├── display.ts -│ └── prompts.ts -├── engine/ # Install engine -│ ├── index.ts +├── install.sh # Bootstrap (Bun + CLI entrypoint) +├── cli/ +│ └── quick-install.ts # Headless/CLI installer +├── engine/ # Install engine (fresh/migrate/update) │ ├── actions.ts +│ ├── build-opencode.ts │ ├── config-gen.ts │ ├── detect.ts +│ ├── index.ts +│ ├── migrate.ts +│ ├── provider-models.ts │ ├── state.ts -│ ├── steps.ts +│ ├── steps-fresh.ts +│ ├── steps-migrate.ts +│ ├── steps-update.ts │ ├── types.ts +│ ├── update.ts │ └── validate.ts -├── electron/ # Electron GUI app -│ ├── main.js -│ ├── package.json -│ └── package-lock.json -├── web/ # Web UI for Electron -│ ├── server.ts -│ └── routes.ts -└── public/ # Static assets - ├── index.html - ├── styles.css - ├── app.js - └── assets/ - ├── pai-logo.png - ├── banner.png - ├── fonts/ - └── audio/ +└── wrapper-template.sh # Optional OpenCode wrapper script template ``` -## Installation Modes +## Notes -### CLI Mode (Default) -Terminal-based interactive installation. - -### GUI Mode -```bash -bash PAI-Install/install.sh --gui -``` -Launches Electron installer with visual step-by-step setup. +- GUI installer was removed. `install.sh` always runs the CLI installer. ## Post-Installation @@ -75,7 +55,7 @@ After installation, you'll have: - `~/.opencode/plugins/` — Event handlers - `~/.opencode/commands/` — Custom OpenCode commands - `~/.opencode/MEMORY/` — Working memory and state -- `~opencode.json` — Configuration with Model Tiers +- `~/.opencode/opencode.json` — OpenCode config with model tiers ## Upgrade from v2.x @@ -87,7 +67,6 @@ See [UPGRADE.md](/UPGRADE.md) for migration instructions. |-------|----------| | Bun not found | Installer will auto-install Bun | | Permission denied | Run with `bash` not `sh` | -| Electron fails | Use CLI mode: `install.sh --cli` | ## Requirements diff --git a/PAI-Install/cli/quick-install.ts b/PAI-Install/cli/quick-install.ts index 24794e8b..7b384c68 100644 --- a/PAI-Install/cli/quick-install.ts +++ b/PAI-Install/cli/quick-install.ts @@ -96,7 +96,7 @@ MIGRATION OPTIONS: EXAMPLES: # Fresh install with Zen (FREE) - bun cli/quick-install.ts --preset zen --name "Steffen" --ai-name "Jeremy" + bun cli/quick-install.ts --preset zen --name "Your Name" --ai-name "Jeremy" # Fresh install with Anthropic API key bun cli/quick-install.ts --preset anthropic --api-key "sk-ant-..." @@ -107,7 +107,7 @@ EXAMPLES: # Update to latest bun cli/quick-install.ts --update -For interactive GUI installation, run: bash install.sh + For installation via bootstrap script, run: bash PAI-Install/install.sh `); process.exit(0); } diff --git a/PAI-Install/electron/main.js b/PAI-Install/electron/main.js deleted file mode 100644 index 63b54934..00000000 --- a/PAI-Install/electron/main.js +++ /dev/null @@ -1,193 +0,0 @@ -/** - * PAI Installer — Electron Wrapper - * Spawns the Bun web server, then opens a frameless window. - * Audio autoplay is enabled (no browser restrictions). - */ - -const { app, BrowserWindow } = require("electron"); -const { spawn } = require("child_process"); -const path = require("path"); -const net = require("net"); - -// Force autoplay at the Chromium level (belt + suspenders with webPreferences) -app.commandLine.appendSwitch("autoplay-policy", "no-user-gesture-required"); - -const PORT = parseInt(process.env.PAI_INSTALL_PORT || "1337"); -const INSTALLER_DIR = path.resolve(__dirname, ".."); - -let serverProcess = null; -let mainWindow = null; - -// ─── Single Instance Lock ──────────────────────────────────────── -// Prevents launching 20 copies of the installer - -const gotLock = app.requestSingleInstanceLock(); -if (!gotLock) { - app.quit(); -} else { - app.on("second-instance", () => { - if (mainWindow) { - if (mainWindow.isMinimized()) mainWindow.restore(); - mainWindow.focus(); - } - }); -} - -// ─── Wait for server to be ready ───────────────────────────────── - -async function waitForServer(port, timeout = 15000) { - const start = Date.now(); - return new Promise((resolve, reject) => { - async function tryConnect() { - if (Date.now() - start > timeout) { - return reject(new Error("Server start timeout")); - } - - // First: check if socket connects - const socket = new net.Socket(); - socket.setTimeout(500); - socket.once("connect", async () => { - socket.destroy(); - - // Second: verify it's actually our Bun server by making HTTP request - try { - const http = require('http'); - const req = http.get(`http://127.0.0.1:${port}/`, (res) => { - let data = ''; - res.on('data', chunk => data += chunk); - res.on('end', () => { - // Check if response contains PAI Installer markers (not just any 200) - const isPAIInstaller = data.includes('PAI') && data.includes('Installer'); - if (res.statusCode === 200 && isPAIInstaller) { - resolve(); - } else { - setTimeout(tryConnect, 200); - } - }); - }); - req.on('error', () => { - setTimeout(tryConnect, 200); - }); - req.setTimeout(1000, () => { - req.destroy(); - setTimeout(tryConnect, 200); - }); - } catch { - setTimeout(tryConnect, 200); - } - }); - socket.once("error", () => { - socket.destroy(); - setTimeout(tryConnect, 200); - }); - socket.once("timeout", () => { - socket.destroy(); - setTimeout(tryConnect, 200); - }); - socket.connect(port, "127.0.0.1"); - } - tryConnect(); - }); -} - -// ─── Start Bun server ──────────────────────────────────────────── - -function startServer() { - const mainTs = path.join(INSTALLER_DIR, "main.ts"); - serverProcess = spawn("bun", ["run", mainTs, "--mode", "web"], { - cwd: INSTALLER_DIR, - env: { ...process.env, PAI_INSTALL_PORT: String(PORT) }, - stdio: ["ignore", "pipe", "pipe"], - }); - - serverProcess.stdout.on("data", (data) => { - process.stdout.write(data); - }); - - serverProcess.stderr.on("data", (data) => { - process.stderr.write(data); - }); - - serverProcess.on("error", (err) => { - console.error("Failed to start server:", err.message); - app.quit(); - }); - - serverProcess.on("exit", (code) => { - if (code !== 0 && code !== null) { - console.error(`Server exited with code ${code}`); - } - }); -} - -// ─── Create Window ─────────────────────────────────────────────── - -function createWindow() { - mainWindow = new BrowserWindow({ - width: 1280, - height: 820, - minWidth: 900, - minHeight: 600, - backgroundColor: "#0f0f14", - title: "PAI Installer", - autoHideMenuBar: true, - webPreferences: { - autoplayPolicy: "no-user-gesture-required", - nodeIntegration: false, - contextIsolation: true, - }, - }); - - mainWindow.loadURL(`http://127.0.0.1:${PORT}/`); - - mainWindow.on("closed", () => { - mainWindow = null; - }); -} - -// ─── App Lifecycle ─────────────────────────────────────────────── - -app.whenReady().then(async () => { - startServer(); - - try { - await waitForServer(PORT); - } catch (err) { - console.error("Could not start installer server:", err.message); - app.quit(); - return; - } - - createWindow(); -}); - -// ─── Server Process Cleanup (Cross-Platform) ────────────────────── - -function killServerProcess(proc) { - try { - if (process.platform === 'win32') { - proc.kill('SIGTERM'); - } else { - // On Unix, kill the process group (negative PID) to catch child processes - process.kill(-proc.pid, 'SIGTERM'); - } - } catch (e) { - // Fallback: kill the process directly - try { proc.kill('SIGTERM'); } catch (_) {} - } -} - -app.on("window-all-closed", () => { - if (serverProcess) { - killServerProcess(serverProcess); - serverProcess = null; - } - app.quit(); -}); - -app.on("before-quit", () => { - if (serverProcess) { - killServerProcess(serverProcess); - serverProcess = null; - } -}); diff --git a/PAI-Install/electron/package-lock.json b/PAI-Install/electron/package-lock.json deleted file mode 100644 index 6c80627e..00000000 --- a/PAI-Install/electron/package-lock.json +++ /dev/null @@ -1,801 +0,0 @@ -{ - "name": "pai-installer", - "version": "4.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "pai-installer", - "version": "4.0.0", - "dependencies": { - "electron": "^34.0.0" - } - }, - "node_modules/@electron/get": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", - "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.1.1", - "env-paths": "^2.2.0", - "fs-extra": "^8.1.0", - "got": "^11.8.5", - "progress": "^2.0.3", - "semver": "^6.2.0", - "sumchecker": "^3.0.1" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "global-agent": "^3.0.0" - } - }, - "node_modules/@sindresorhus/is": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", - "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" - } - }, - "node_modules/@szmarczak/http-timer": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", - "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", - "license": "MIT", - "dependencies": { - "defer-to-connect": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@types/cacheable-request": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", - "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", - "license": "MIT", - "dependencies": { - "@types/http-cache-semantics": "*", - "@types/keyv": "^3.1.4", - "@types/node": "*", - "@types/responselike": "^1.0.0" - } - }, - "node_modules/@types/http-cache-semantics": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", - "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", - "license": "MIT" - }, - "node_modules/@types/keyv": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", - "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/node": { - "version": "20.19.33", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.33.tgz", - "integrity": "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==", - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@types/responselike": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", - "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "license": "MIT", - "optional": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/boolean": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", - "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", - "license": "MIT", - "optional": true - }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/cacheable-lookup": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", - "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", - "license": "MIT", - "engines": { - "node": ">=10.6.0" - } - }, - "node_modules/cacheable-request": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", - "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", - "license": "MIT", - "dependencies": { - "clone-response": "^1.0.2", - "get-stream": "^5.1.0", - "http-cache-semantics": "^4.0.0", - "keyv": "^4.0.0", - "lowercase-keys": "^2.0.0", - "normalize-url": "^6.0.1", - "responselike": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/clone-response": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", - "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", - "license": "MIT", - "dependencies": { - "mimic-response": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "license": "MIT", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/decompress-response/node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/defer-to-connect": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", - "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "license": "MIT", - "optional": true, - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "license": "MIT", - "optional": true, - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/detect-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", - "license": "MIT", - "optional": true - }, - "node_modules/electron": { - "version": "34.5.8", - "resolved": "https://registry.npmjs.org/electron/-/electron-34.5.8.tgz", - "integrity": "sha512-vxLD65mabTzYmEVa9KceMHM0+zO+vqgrhcyNVlmTd0IGV5J7XZ8v/qElm0o4YQ4wPeq7olZkUjZkBQQEdr23/g==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "@electron/get": "^2.0.0", - "@types/node": "^20.9.0", - "extract-zip": "^2.0.1" - }, - "bin": { - "electron": "cli.js" - }, - "engines": { - "node": ">= 12.20.55" - } - }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es6-error": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", - "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", - "license": "MIT", - "optional": true - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "license": "BSD-2-Clause", - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "license": "MIT", - "dependencies": { - "pend": "~1.2.0" - } - }, - "node_modules/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, - "node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "license": "MIT", - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/global-agent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", - "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", - "license": "BSD-3-Clause", - "optional": true, - "dependencies": { - "boolean": "^3.0.1", - "es6-error": "^4.1.1", - "matcher": "^3.0.0", - "roarr": "^2.15.3", - "semver": "^7.3.2", - "serialize-error": "^7.0.1" - }, - "engines": { - "node": ">=10.0" - } - }, - "node_modules/global-agent/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "license": "ISC", - "optional": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/globalthis": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "define-properties": "^1.2.1", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/got": { - "version": "11.8.6", - "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", - "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", - "license": "MIT", - "dependencies": { - "@sindresorhus/is": "^4.0.0", - "@szmarczak/http-timer": "^4.0.5", - "@types/cacheable-request": "^6.0.1", - "@types/responselike": "^1.0.0", - "cacheable-lookup": "^5.0.3", - "cacheable-request": "^7.0.2", - "decompress-response": "^6.0.0", - "http2-wrapper": "^1.0.0-beta.5.2", - "lowercase-keys": "^2.0.0", - "p-cancelable": "^2.0.0", - "responselike": "^2.0.0" - }, - "engines": { - "node": ">=10.19.0" - }, - "funding": { - "url": "https://github.com/sindresorhus/got?sponsor=1" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "license": "ISC" - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "license": "MIT", - "optional": true, - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/http-cache-semantics": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", - "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", - "license": "BSD-2-Clause" - }, - "node_modules/http2-wrapper": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", - "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", - "license": "MIT", - "dependencies": { - "quick-lru": "^5.1.1", - "resolve-alpn": "^1.0.0" - }, - "engines": { - "node": ">=10.19.0" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "license": "MIT" - }, - "node_modules/json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", - "license": "ISC", - "optional": true - }, - "node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "license": "MIT", - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/lowercase-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", - "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/matcher": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", - "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", - "license": "MIT", - "optional": true, - "dependencies": { - "escape-string-regexp": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/mimic-response": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/normalize-url": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", - "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/p-cancelable": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", - "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "license": "MIT" - }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/pump": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", - "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/resolve-alpn": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", - "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", - "license": "MIT" - }, - "node_modules/responselike": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", - "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", - "license": "MIT", - "dependencies": { - "lowercase-keys": "^2.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/roarr": { - "version": "2.15.4", - "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", - "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", - "license": "BSD-3-Clause", - "optional": true, - "dependencies": { - "boolean": "^3.0.1", - "detect-node": "^2.0.4", - "globalthis": "^1.0.1", - "json-stringify-safe": "^5.0.1", - "semver-compare": "^1.0.0", - "sprintf-js": "^1.1.2" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/semver-compare": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", - "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", - "license": "MIT", - "optional": true - }, - "node_modules/serialize-error": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", - "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", - "license": "MIT", - "optional": true, - "dependencies": { - "type-fest": "^0.13.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/sprintf-js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", - "license": "BSD-3-Clause", - "optional": true - }, - "node_modules/sumchecker": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", - "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", - "license": "Apache-2.0", - "dependencies": { - "debug": "^4.1.0" - }, - "engines": { - "node": ">= 8.0" - } - }, - "node_modules/type-fest": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", - "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", - "license": "(MIT OR CC0-1.0)", - "optional": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "license": "MIT" - }, - "node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "license": "MIT", - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "license": "MIT", - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - } - } -} diff --git a/PAI-Install/electron/package.json b/PAI-Install/electron/package.json deleted file mode 100644 index 1e4c2c40..00000000 --- a/PAI-Install/electron/package.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "pai-installer", - "version": "4.0.3", - "description": "PAI Installer — Electron wrapper", - "main": "main.js", - "scripts": { - "start": "electron ." - }, - "dependencies": { - "electron": "^35.7.5" - } -} diff --git a/PAI-Install/engine/build-opencode.ts b/PAI-Install/engine/build-opencode.ts index ff85a621..210062c9 100644 --- a/PAI-Install/engine/build-opencode.ts +++ b/PAI-Install/engine/build-opencode.ts @@ -5,7 +5,7 @@ * Builds custom OpenCode binary from Steffen025/opencode fork * with feature/model-tiers branch for 60x cost optimization. * - * Based on: PAIOpenCodeWizard.ts (port) + * Based on a legacy interactive installer implementation (removed). * Reference: ~/.opencode/tools/opencode-wrapper (bash implementation) */ @@ -242,4 +242,4 @@ export async function getBuildStatus(): Promise<{ export async function isHomebrewAvailable(): Promise { return existsSync(BREW_BIN_PATH); -} \ No newline at end of file +} diff --git a/PAI-Install/engine/provider-models.ts b/PAI-Install/engine/provider-models.ts index ef3b1bc9..69362643 100644 --- a/PAI-Install/engine/provider-models.ts +++ b/PAI-Install/engine/provider-models.ts @@ -44,7 +44,7 @@ export const PROVIDER_MODELS: Record = { }; /** - * Human-readable labels shown in the installer wizard. + * Human-readable labels shown in the installer. */ export const PROVIDER_LABELS: Record = { anthropic: { diff --git a/PAI-Install/generate-welcome.ts b/PAI-Install/generate-welcome.ts deleted file mode 100644 index adb0227e..00000000 --- a/PAI-Install/generate-welcome.ts +++ /dev/null @@ -1,114 +0,0 @@ -#!/usr/bin/env bun -/** - * PAI Installer v4.0 — Welcome MP3 Generator - * Uses ElevenLabs API to generate the welcome audio with a voice clone. - * - * Usage: bun generate-welcome.ts - * - * Requires: ELEVENLABS_API_KEY environment variable - * Uses voice clone ID from settings.json principal.voiceClone - */ - -import { writeFileSync, readFileSync, existsSync, mkdirSync } from "fs"; -import { join } from "path"; -import { homedir } from "os"; - -const OUTPUT_PATH = join(import.meta.dir, "public", "assets", "welcome.mp3"); - -// Voice ID — check env var, then settings.json voices, then default -function getVoiceId(): string { - // Environment variable takes priority - if (process.env.ELEVENLABS_VOICE_ID) return process.env.ELEVENLABS_VOICE_ID; - - const settingsPath = join(homedir(), ".opencode", "settings.json"); - if (existsSync(settingsPath)) { - try { - const settings = JSON.parse(readFileSync(settingsPath, "utf-8")); - // Use principal's voice clone (the installer speaks in the user's voice) - const clone = settings.principal?.voiceClone; - if (typeof clone === "string") return clone; - if (typeof clone?.voiceId === "string") return clone.voiceId; - // Fallback to DA main voice - const mainVoice = settings.daidentity?.voices?.main?.voiceId; - if (typeof mainVoice === "string") return mainVoice; - } catch (err) { - // Log warning but continue to fallback - console.warn(`Warning: Could not parse settings.json: ${err instanceof Error ? err.message : err}`); - } - } - // Fallback to a default ElevenLabs voice - return "pNInz6obpgDQGcFmaJgB"; // Adam -} - -async function generateWelcome() { - const apiKey = process.env.ELEVENLABS_API_KEY; - if (!apiKey) { - // Try to read from config - const envPath = join(homedir(), ".config", "PAI", ".env"); - if (existsSync(envPath)) { - const envContent = readFileSync(envPath, "utf-8"); - const match = envContent.match(/ELEVENLABS_API_KEY=["']?([^"'\n]+)["']?/); - if (match) { - process.env.ELEVENLABS_API_KEY = match[1].trim(); - } - } - - if (!process.env.ELEVENLABS_API_KEY) { - console.error("Error: ELEVENLABS_API_KEY not found in environment or ~/.config/PAI/.env"); - console.error("Set it with: export ELEVENLABS_API_KEY=your-key-here"); - process.exit(1); - } - } - - const voiceId = getVoiceId(); - const text = "Welcome to Personal AI Infrastructure. Magnifying human capabilities."; - - console.log(`Generating welcome audio...`); - console.log(` Voice ID: ${voiceId}`); - console.log(` Text: "${text}"`); - console.log(` Output: ${OUTPUT_PATH}`); - - const response = await fetch( - `https://api.elevenlabs.io/v1/text-to-speech/${voiceId}`, - { - method: "POST", - headers: { - "xi-api-key": process.env.ELEVENLABS_API_KEY!, - "Content-Type": "application/json", - }, - body: JSON.stringify({ - text, - model_id: "eleven_turbo_v2_5", - voice_settings: { - stability: 0.85, - similarity_boost: 0.9, - style: 0.1, - use_speaker_boost: true, - }, - }), - } - ); - - if (!response.ok) { - const error = await response.text(); - console.error(`ElevenLabs API error (${response.status}): ${error}`); - process.exit(1); - } - - const buffer = await response.arrayBuffer(); - - // Ensure output directory exists - const outputDir = join(import.meta.dir, "public", "assets"); - if (!existsSync(outputDir)) { - mkdirSync(outputDir, { recursive: true }); - } - - writeFileSync(OUTPUT_PATH, Buffer.from(buffer)); - - console.log(`\n✓ Welcome audio generated: ${OUTPUT_PATH} (${Math.round(buffer.byteLength / 1024)}KB)`); -} - -generateWelcome().catch((err) => { - console.error("Error:", err instanceof Error ? err.message : String(err)); - process.exit(1); -}); diff --git a/PAI-Install/install.sh b/PAI-Install/install.sh index 1d464d66..27bb066d 100755 --- a/PAI-Install/install.sh +++ b/PAI-Install/install.sh @@ -1,11 +1,11 @@ #!/usr/bin/env bash # PAI-OpenCode Installer Bootstrap # -# WHY: Single entry point for both GUI and headless installation. +# WHY: Single entry point for deterministic CLI installation. # # Usage: -# bash install.sh # Launch Electron GUI (default) -# bash install.sh --cli [args...] # Headless installation +# bash install.sh [args...] # CLI installation (default) +# bash install.sh --cli [args...] # CLI installation (alias) # set -euo pipefail @@ -51,31 +51,21 @@ else warn "OpenCode not found — will install during setup" fi -# ─── Launch Installer ──────────────────────────────────── -# Resolve PAI-Install directory (may be sibling or child of script location) -INSTALLER_DIR="" -if [ -d "$SCRIPT_DIR/PAI-Install" ]; then - INSTALLER_DIR="$SCRIPT_DIR/PAI-Install" -elif [ -f "$SCRIPT_DIR/main.ts" ]; then - INSTALLER_DIR="$SCRIPT_DIR" -else - error "Cannot find PAI-Install directory. Expected at: $SCRIPT_DIR/PAI-Install/" - exit 1 -fi +# ─── Launch Installer (CLI only) ───────────────────────── +INSTALLER_DIR="$SCRIPT_DIR" -info "Launching installer..." +info "Launching CLI installer..." echo "" -# Launch mode +# Backwards-compatible alias if [ "${1:-}" = "--cli" ]; then - # Headless mode shift - exec bun "$INSTALLER_DIR/cli/quick-install.ts" "$@" -else - # GUI mode (default) - runs from electron subdirectory - cd "$INSTALLER_DIR/electron" - if ! bun install 2>&1 | tee /tmp/pai-install-deps.log; then - echo "⚠️ Warning: bun install had issues. Check /tmp/pai-install-deps.log" - fi - exec bunx electron . fi + +# Hard fail if someone tries to use the removed GUI mode +if [ "${1:-}" = "--gui" ] || [ "${1:-}" = "--mode" ]; then + error "GUI mode was removed. Use CLI options." + exec bun "$INSTALLER_DIR/cli/quick-install.ts" --help +fi + +exec bun "$INSTALLER_DIR/cli/quick-install.ts" "$@" diff --git a/PAI-Install/main.ts b/PAI-Install/main.ts deleted file mode 100644 index b38a2537..00000000 --- a/PAI-Install/main.ts +++ /dev/null @@ -1,82 +0,0 @@ -#!/usr/bin/env bun -/** - * PAI Installer v4.0 — Main Entry Point - * Routes to CLI, Web server (for Electron), or GUI (Electron app). - * - * Modes: - * --mode cli → Interactive terminal wizard - * --mode web → Start HTTP/WebSocket server (used internally by Electron) - * --mode gui → Launch Electron app (which spawns web mode internally) - */ - -import { spawn, spawnSync, execSync } from "child_process"; -import { join } from "path"; -import { existsSync } from "fs"; - -const args = process.argv.slice(2); -const modeIdx = args.indexOf("--mode"); -const rawMode = modeIdx >= 0 ? args[modeIdx + 1] : "gui"; -const validModes = ["cli", "web", "gui"]; -const mode = validModes.includes(rawMode) ? rawMode : "gui"; - -const ROOT = import.meta.dir; - -async function main() { - if (mode === "cli") { - // Run CLI wizard - const { runCLI } = await import("./cli/index"); - await runCLI(); - } else if (mode === "web") { - // Start the HTTP + WebSocket server (Electron loads this) - await import("./web/server"); - } else { - // Launch Electron GUI app - const electronDir = join(ROOT, "electron"); - const electronLock = join(electronDir, "bun.lock"); - - // Install electron dependencies if needed - if (!existsSync(electronLock)) { - console.log("Installing GUI dependencies (first run only)...\n"); - const install = spawnSync("bun", ["install"], { - cwd: electronDir, - stdio: "inherit", - }); - if (install.status !== 0) { - console.error("Failed to install GUI dependencies. Falling back to CLI...\n"); - const { runCLI } = await import("./cli/index"); - await runCLI(); - return; - } - } - - // Clear macOS quarantine flags (prevents "app is damaged" error on copied installs) - if (process.platform === "darwin") { - try { - execSync(`xattr -cr "${electronDir}"`, { stdio: "pipe", timeout: 30000 }); - console.log("Cleared macOS quarantine flags.\n"); - } catch { - // Non-fatal - } - } - - console.log("Starting PAI Installer GUI...\n"); - const child = spawn("bunx", ["electron", "."], { - cwd: electronDir, - stdio: "inherit", - }); - - child.on("error", (err) => { - console.error("Failed to start GUI:", err); - process.exit(1); - }); - - child.on("exit", (code) => { - process.exit(code || 0); - }); - } -} - -main().catch((err) => { - console.error("Fatal error:", err.message); - process.exit(1); -}); diff --git a/PAI-Install/public/app.js b/PAI-Install/public/app.js deleted file mode 100644 index e627cb76..00000000 --- a/PAI-Install/public/app.js +++ /dev/null @@ -1,702 +0,0 @@ -/** - * PAI Installer v4.0 — Frontend Application - * Vanilla JavaScript — no framework dependencies. - * Handles WebSocket communication, UI rendering, and state management. - */ - -// ─── State ─────────────────────────────────────────────────────── - -let ws = null; -let connected = false; -let voiceEnabled = true; -let currentAudio = null; -let installMode = null; // 'fresh', 'migrate', 'update' -let steps = []; -let installationComplete = false; - -// ─── WebSocket Connection ──────────────────────────────────────── - -function connect() { - if (installationComplete) return; - const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:'; - ws = new WebSocket(`${protocol}//${location.host}/ws`); - - ws.onopen = () => { - connected = true; - ws.send(JSON.stringify({ type: 'client_ready' })); - }; - - ws.onmessage = (event) => { - try { - const msg = JSON.parse(event.data); - handleServerMessage(msg); - } catch (err) { - console.error('Failed to parse WebSocket message:', err); - } - }; - - ws.onclose = () => { - connected = false; - if (!installationComplete) { - addMessage('system', 'Connection lost. Reconnecting...', false); - setTimeout(connect, 2000); // Auto-reconnect - } - }; - - ws.onerror = () => { - connected = false; - }; -} - -// ─── Message Handler ───────────────────────────────────────────── - -function handleServerMessage(msg) { - const isReplayed = msg.replayed === true; - - switch (msg.type) { - case 'connected': - break; - - case 'mode_detected': - installMode = msg.mode; - renderModeSelection(msg.mode); - break; - - case 'mode_selected': - installationComplete = false; - setStepsForMode(msg.mode); - renderSteps(); - break; - - case 'step_update': - updateStep(msg.step, msg.status); - updateProgress(); - // Remove the spinning progress bar for this step when it completes or is skipped - if (msg.status === 'completed' || msg.status === 'skipped' || msg.status === 'failed') { - const progressEl = document.getElementById('progress-' + msg.step); - if (progressEl) { - // Fill bar to 100% and stop spinner visually before removing - const fill = progressEl.querySelector('.mini-fill'); - if (fill) fill.style.width = '100%'; - const spinner = progressEl.querySelector('.spinner'); - if (spinner) spinner.style.animationPlayState = 'paused'; - // Fade out and remove after short delay so user sees completion - progressEl.style.transition = 'opacity 0.4s'; - progressEl.style.opacity = '0'; - setTimeout(() => { if (progressEl.parentNode) progressEl.parentNode.removeChild(progressEl); }, 450); - } - } - break; - - case 'detection_result': - renderDetection(msg.data); - break; - - case 'message': - addMessage(msg.role || 'assistant', msg.content, isReplayed); - if (msg.speak && !isReplayed && voiceEnabled) { - // TTS would go here if we had the ElevenLabs key - } - break; - - case 'input_request': - renderInputForm(msg.id, msg.prompt, msg.inputType, msg.placeholder); - break; - - case 'choice_request': - renderChoiceForm(msg.id, msg.prompt, msg.choices); - break; - - case 'progress': - renderProgress(msg.step, msg.percent, msg.detail); - break; - - case 'validation_result': - renderValidation(msg.checks); - break; - - case 'install_complete': - installationComplete = true; - renderSummary(msg.summary); - break; - - case 'error': - addMessage('error', `Error: ${msg.message}`, isReplayed); - break; - } - - scrollToBottom(); -} - -// ─── Step Management ───────────────────────────────────────────── - -function updateStep(stepId, status) { - const step = steps.find(s => s.id === stepId); - if (step) step.status = status; - renderSteps(); -} - -function updateProgress() { - const done = steps.filter(s => s.status === 'completed' || s.status === 'skipped').length; - const pct = Math.round((done / steps.length) * 100); - - const fill = document.getElementById('progress-fill'); - const text = document.getElementById('progress-text'); - const sidebarFill = document.getElementById('sidebar-progress-fill'); - const sidebarText = document.getElementById('sidebar-progress-text'); - - if (fill) fill.style.width = pct + '%'; - if (text) text.textContent = `Step ${done}/${steps.length}`; - if (sidebarFill) sidebarFill.style.width = pct + '%'; - if (sidebarText) sidebarText.textContent = `Progress: ${pct}%`; -} - -function renderSteps() { - const list = document.getElementById('step-list'); - if (!list) return; - - list.innerHTML = ''; - - for (const s of steps) { - let icon = '○'; - let cls = s.status; - if (s.status === 'completed') icon = '✓'; - else if (s.status === 'active') icon = '→'; - else if (s.status === 'skipped') icon = '–'; - else if (s.status === 'failed') icon = '✗'; - - const li = document.createElement('li'); - li.className = `step-item ${cls}`; - - const iconSpan = document.createElement('span'); - iconSpan.className = `step-icon ${cls}`; - iconSpan.textContent = icon; - - const labelSpan = document.createElement('span'); - labelSpan.className = 'step-label'; - labelSpan.textContent = `${s.number}. ${s.name}`; - - li.appendChild(iconSpan); - li.appendChild(labelSpan); - list.appendChild(li); - } -} - -// ─── Chat Rendering ────────────────────────────────────────────── - -function addMessage(role, content, replayed) { - if (!content || !content.trim()) return; - const chat = document.getElementById('chat-messages'); - if (!chat) return; - - const div = document.createElement('div'); - div.className = `msg ${role}`; - div.textContent = content; - if (replayed) div.style.animation = 'none'; - chat.appendChild(div); -} - -function renderDetection(data) { - const chat = document.getElementById('chat-messages'); - if (!chat) return; - - const items = [ - { icon: 'check', label: 'OS', value: data.os?.name + ' (' + data.os?.arch + ')' }, - { icon: 'check', label: 'Shell', value: data.shell?.name }, - { icon: data.tools?.bun?.installed ? 'check' : 'cross', label: 'Bun', value: data.tools?.bun?.installed ? 'v' + data.tools.bun.version : 'Not found' }, - { icon: data.tools?.git?.installed ? 'check' : 'cross', label: 'Git', value: data.tools?.git?.installed ? 'v' + data.tools.git.version : 'Not found' }, - { icon: data.tools?.opencode?.installed ? 'check' : 'info', label: 'OpenCode', value: data.tools?.opencode?.installed ? 'v' + data.tools.opencode.version : 'Will install' }, - { icon: 'info', label: 'Timezone', value: data.timezone }, - { icon: data.existing?.paiInstalled ? 'info' : 'check', label: 'Existing PAI', value: data.existing?.paiInstalled ? 'v' + (data.existing.paiVersion || '?') : 'Fresh install' }, - { icon: data.existing?.hasApiKeys ? 'check' : 'info', label: 'ElevenLabs Key', value: data.existing?.elevenLabsKeyFound ? 'Found' : 'Not found' }, - ]; - - const grid = document.createElement('div'); - grid.className = 'detection-grid'; - - items.forEach(i => { - const item = document.createElement('div'); - item.className = 'detection-item'; - - const iconSpan = document.createElement('span'); - iconSpan.className = i.icon; - iconSpan.textContent = i.icon === 'check' ? '✓' : i.icon === 'cross' ? '✗' : 'ℹ'; - - const labelSpan = document.createElement('span'); - labelSpan.textContent = `${i.label}: ${i.value}`; - - item.appendChild(iconSpan); - item.appendChild(labelSpan); - grid.appendChild(item); - }); - - chat.appendChild(grid); -} - -function renderInputForm(requestId, prompt, inputType, placeholder) { - const chat = document.getElementById('chat-messages'); - if (!chat) return; - - // Show prompt as message - addMessage('assistant', prompt); - - const form = document.createElement('div'); - form.className = 'inline-form'; - - const label = document.createElement('label'); - label.textContent = inputType === 'key' ? 'API Key' : 'Input'; - - const formRow = document.createElement('div'); - formRow.className = 'form-row'; - - const input = document.createElement('input'); - input.className = 'inline-input'; - input.type = inputType === 'key' || inputType === 'password' ? 'password' : 'text'; - input.placeholder = placeholder || ''; - input.id = `input-${requestId}`; - input.autocomplete = 'off'; - - const button = document.createElement('button'); - button.className = 'inline-btn'; - button.textContent = 'Submit'; - button.onclick = () => submitInput(requestId); - - formRow.appendChild(input); - formRow.appendChild(button); - form.appendChild(label); - form.appendChild(formRow); - chat.appendChild(form); - - // Focus input - setTimeout(() => { - const input = document.getElementById('input-' + requestId); - if (input) { - input.focus(); - input.addEventListener('keydown', (e) => { - if (e.key === 'Enter') submitInput(requestId); - }); - } - }, 100); - - scrollToBottom(); -} - -function submitInput(requestId) { - const input = document.getElementById('input-' + requestId); - if (!input) return; - - const value = input.value.trim(); - if (!value && input.getAttribute('type') === 'password') { - // Allow empty for optional fields - } - - // Check WebSocket state before sending - if (!ws || ws.readyState !== WebSocket.OPEN) { - addMessage('system', 'Connection lost. Please wait for reconnect...', false); - return; - } - - // Mask key display - for passwords or API keys - let display; - if (input.getAttribute('type') === 'password') { - display = '•••••'; - } else if (value.startsWith('sk-') || value.startsWith('xi-')) { - display = value.substring(0, 8) + '...'; - } else { - display = value; - } - addMessage('user', display || '(empty)'); - - // Disable form - input.disabled = true; - input.closest('.inline-form').querySelector('.inline-btn').disabled = true; - - ws.send(JSON.stringify({ type: 'user_input', requestId, value })); - scrollToBottom(); -} - -function renderChoiceForm(requestId, prompt, choices) { - const chat = document.getElementById('chat-messages'); - if (!chat) return; - - addMessage('assistant', prompt); - - // Voice preview audio map — show previews for both initial and retry voice selection - const voicePreviews = { female: '/assets/voice-female.mp3', male: '/assets/voice-male.mp3' }; - const isVoiceTypeRequest = requestId === 'voice-type' || requestId === 'voice-type-retry'; - - const group = document.createElement('div'); - group.className = 'choice-group'; - - choices.forEach(c => { - const btn = document.createElement('button'); - btn.className = 'choice-btn'; - btn.dataset.requestId = requestId; - btn.dataset.value = c.value; - - const labelSpan = document.createElement('span'); - labelSpan.className = 'choice-label'; - labelSpan.textContent = c.label; - btn.appendChild(labelSpan); - - if (c.description) { - const descSpan = document.createElement('span'); - descSpan.className = 'choice-desc'; - descSpan.textContent = c.description; - btn.appendChild(descSpan); - } - - btn.addEventListener('click', () => submitChoice(requestId, c.value, btn)); - group.appendChild(btn); - - // Add preview button as sibling (not nested) for voice selection - if (voicePreviews[c.value] && isVoiceTypeRequest) { - const preview = document.createElement('button'); - preview.type = 'button'; - preview.className = 'preview-btn'; - preview.innerHTML = '▶ Preview'; - preview.addEventListener('click', (e) => { - e.stopPropagation(); - playPreview(voicePreviews[c.value], preview); - }); - group.appendChild(preview); - } - }); - - chat.appendChild(group); - scrollToBottom(); -} - -function playPreview(src, btn) { - if (currentAudio) { currentAudio.pause(); currentAudio = null; } - currentAudio = new Audio(src); - currentAudio.volume = 0.8; - currentAudio.play().catch(() => {}); - btn.textContent = '⏹ Playing'; - currentAudio.onended = () => { btn.textContent = '▶ Preview'; currentAudio = null; }; -} - -function submitChoice(requestId, value, btn) { - // Check WebSocket state before sending - if (!ws || ws.readyState !== WebSocket.OPEN) { - addMessage('system', 'Connection lost. Please wait for reconnect...', false); - return; - } - - // Highlight selected, disable all - const group = btn.closest('.choice-group'); - group.querySelectorAll('.choice-btn').forEach(b => { - b.disabled = true; - b.style.opacity = b === btn ? '1' : '0.4'; - }); - btn.style.borderColor = 'var(--accent-primary)'; - - // Extract only the label text, not description or preview button text - const labelEl = btn.querySelector('.choice-label'); - const displayText = labelEl ? labelEl.textContent.trim() : btn.textContent.trim().split('\n')[0]; - addMessage('user', displayText); - ws.send(JSON.stringify({ type: 'user_choice', requestId, value })); - scrollToBottom(); -} - -function renderProgress(step, percent, detail) { - // Update or create progress indicator - let existing = document.getElementById('progress-' + step); - const chat = document.getElementById('chat-messages'); - - if (existing) { - existing.querySelector('.mini-fill').style.width = percent + '%'; - existing.querySelector('.prog-detail').textContent = detail; - } else { - const div = document.createElement('div'); - div.className = 'progress-msg'; - div.id = 'progress-' + step; - - const spinner = document.createElement('div'); - spinner.className = 'spinner'; - - const miniBar = document.createElement('div'); - miniBar.className = 'mini-bar'; - - const miniFill = document.createElement('div'); - miniFill.className = 'mini-fill'; - miniFill.style.width = percent + '%'; - - const progDetail = document.createElement('span'); - progDetail.className = 'prog-detail'; - progDetail.textContent = detail; - - miniBar.appendChild(miniFill); - div.appendChild(spinner); - div.appendChild(miniBar); - div.appendChild(progDetail); - chat.appendChild(div); - } - scrollToBottom(); -} - -function renderValidation(checks) { - const chat = document.getElementById('chat-messages'); - if (!chat) return; - - addMessage('system', 'Running validation checks...'); - - const list = document.createElement('div'); - list.className = 'validation-list'; - - checks.forEach(c => { - const item = document.createElement('div'); - item.className = 'validation-item'; - - const icon = document.createElement('span'); - icon.className = 'v-icon'; - icon.textContent = c.passed ? '✓' : c.critical ? '✗' : '⚠'; - - const name = document.createElement('span'); - name.className = 'v-name'; - name.textContent = c.name; - - const detail = document.createElement('span'); - detail.className = 'v-detail'; - detail.textContent = c.detail; - - item.appendChild(icon); - item.appendChild(name); - item.appendChild(detail); - list.appendChild(item); - }); - - chat.appendChild(list); - scrollToBottom(); -} - -function renderSummary(summary) { - const chat = document.getElementById('chat-messages'); - if (!chat) return; - - const card = document.createElement('div'); - card.className = 'summary-card'; - - const h3 = document.createElement('h3'); - h3.textContent = 'Installation Complete'; - card.appendChild(h3); - - function addRow(label, value) { - const row = document.createElement('div'); - row.className = 'summary-row'; - const labelSpan = document.createElement('span'); - labelSpan.className = 's-label'; - labelSpan.textContent = label; - const valueSpan = document.createElement('span'); - valueSpan.className = 's-value'; - valueSpan.textContent = value; - row.appendChild(labelSpan); - row.appendChild(valueSpan); - card.appendChild(row); - } - - addRow('PAI Version', `v${summary.paiVersion}`); - addRow('Principal', summary.principalName); - addRow('AI Name', summary.aiName); - addRow('Timezone', summary.timezone); - addRow('Voice', summary.voiceEnabled ? summary.voiceMode : 'Disabled'); - addRow('Install Type', summary.mode || summary.installType); - - const actionDiv = document.createElement('div'); - actionDiv.className = 'summary-action'; - const p1 = document.createElement('p'); - p1.textContent = 'To activate PAI, open a terminal and run:'; - const code = document.createElement('code'); - // Use activation command from backend, or derive from detected shell/platform - let activationCommand; - if (summary.activationCommand) { - activationCommand = summary.activationCommand; - } else if (summary.userShell?.includes('bash')) { - activationCommand = 'source ~/.bashrc && pai'; - } else if (summary.userShell?.includes('fish')) { - activationCommand = 'source ~/.config/fish/config.fish && pai'; - } else if (navigator.platform?.includes('Win') || navigator.userAgent?.includes('Windows')) { - // Windows platform detected - activationCommand = 'Restart your terminal and type: pai'; - } else { - // Default Unix/zsh - activationCommand = 'source ~/.zshrc && pai'; - } - code.textContent = activationCommand; - const p2 = document.createElement('p'); - p2.className = 'summary-hint'; - p2.textContent = 'This reloads your shell config and launches PAI for the first time.'; - actionDiv.appendChild(p1); - actionDiv.appendChild(code); - actionDiv.appendChild(p2); - card.appendChild(actionDiv); - - chat.appendChild(card); - scrollToBottom(); -} - -// ─── Welcome Screen ────────────────────────────────────────────── - -function startInstall() { - // This is now handled by selectMode - console.log('Start install clicked - should use selectMode instead'); -} - -// Legacy - keep for compatibility but mode selection handles this now -function legacyStartInstall() { - const overlay = document.getElementById('welcome-overlay'); - if (overlay) overlay.classList.add('hidden'); - - // Start installation only when WebSocket is ready - if (ws && ws.readyState === WebSocket.OPEN) { - ws.send(JSON.stringify({ type: 'start_install' })); - } else { - // Queue for when connection opens with retry limit - let attempts = 0; - const maxRetries = 50; // 5 seconds total (50 * 100ms) - - const checkAndSend = () => { - attempts++; - if (ws && ws.readyState === WebSocket.OPEN) { - ws.send(JSON.stringify({ type: 'start_install' })); - } else if (attempts < maxRetries) { - setTimeout(checkAndSend, 100); - } else { - console.error('Failed to start installation: WebSocket not ready after 5 seconds'); - addMessage('system', 'Error: Could not connect to installer. Please refresh and try again.'); - } - }; - checkAndSend(); - } -} - -// ─── Utilities ─────────────────────────────────────────────────── - -function setStepsForMode(mode) { - if (mode === 'fresh') { - steps = [ - { id: 'system-detect', name: 'System Detection', number: 1, status: 'pending' }, - { id: 'prerequisites', name: 'Prerequisites', number: 2, status: 'pending' }, - { id: 'api-keys', name: 'API Keys', number: 3, status: 'pending' }, - { id: 'identity', name: 'Identity', number: 4, status: 'pending' }, - { id: 'repository', name: 'PAI Repository', number: 5, status: 'pending' }, - { id: 'configuration', name: 'Configuration', number: 6, status: 'pending' }, - { id: 'voice', name: 'DA Voice', number: 7, status: 'pending' }, - { id: 'validation', name: 'Validation', number: 8, status: 'pending' }, - ]; - } else if (mode === 'migrate') { - steps = [ - { id: 'backup', name: 'Backup v2 Config', number: 1, status: 'pending' }, - { id: 'detect', name: 'Detect Current Install', number: 2, status: 'pending' }, - { id: 'migrate-config', name: 'Migrate Configuration', number: 3, status: 'pending' }, - { id: 'build', name: 'Build OpenCode', number: 4, status: 'pending' }, - { id: 'verify', name: 'Verify Migration', number: 5, status: 'pending' }, - ]; - } else if (mode === 'update') { - steps = [ - { id: 'backup', name: 'Backup Current Config', number: 1, status: 'pending' }, - { id: 'pull', name: 'Pull Latest Changes', number: 2, status: 'pending' }, - { id: 'rebuild', name: 'Rebuild & Verify', number: 3, status: 'pending' }, - ]; - } -} - -function renderModeSelection(detectedMode) { - const overlay = document.getElementById('welcome-overlay'); - if (!overlay) return; - - // Clear the default content - overlay.innerHTML = ''; - - const logo = document.createElement('img'); - logo.src = '/assets/pai-logo.png'; - logo.alt = 'PAI'; - logo.className = 'welcome-logo'; - overlay.appendChild(logo); - - const title = document.createElement('div'); - title.className = 'welcome-title'; - title.textContent = 'PAI Installer'; - overlay.appendChild(title); - - const subtitle = document.createElement('div'); - subtitle.className = 'welcome-subtitle'; - subtitle.textContent = 'Personal AI Infrastructure v4.0'; - overlay.appendChild(subtitle); - - // Mode selection - const modeLabel = document.createElement('div'); - modeLabel.style.cssText = 'margin: 20px 0 10px; color: var(--text-secondary); font-size: 14px;'; - modeLabel.textContent = detectedMode === 'fresh' - ? 'No existing installation found' - : detectedMode === 'migrate' - ? 'Existing v2 installation detected' - : 'Existing v3 installation detected'; - overlay.appendChild(modeLabel); - - const buttonGroup = document.createElement('div'); - buttonGroup.style.cssText = 'display: flex; gap: 10px; margin-top: 20px;'; - - if (detectedMode === 'fresh') { - // Only fresh install option - const freshBtn = createModeButton('Fresh Install', 'New installation with full setup', 'fresh', true); - buttonGroup.appendChild(freshBtn); - } else if (detectedMode === 'migrate') { - // v2 -> v3 migration options - const migrateBtn = createModeButton('Migrate from v2', 'Migrate your existing v2 configuration to v3', 'migrate', true); - const freshBtn = createModeButton('Fresh Install', 'Start fresh (discards v2 config)', 'fresh', false); - buttonGroup.appendChild(migrateBtn); - buttonGroup.appendChild(freshBtn); - } else if (detectedMode === 'update') { - // v3 update options - const updateBtn = createModeButton('Update', 'Update to latest v3.x version', 'update', true); - const freshBtn = createModeButton('Reinstall Fresh', 'Remove and reinstall fresh', 'fresh', false); - buttonGroup.appendChild(updateBtn); - buttonGroup.appendChild(freshBtn); - } - - overlay.appendChild(buttonGroup); -} - -function createModeButton(label, description, mode, isPrimary) { - const btn = document.createElement('button'); - btn.className = isPrimary ? 'welcome-start' : 'welcome-start secondary'; - btn.style.cssText = isPrimary ? '' : 'background: transparent; border: 1px solid var(--accent-primary); color: var(--accent-primary);'; - btn.innerHTML = `
${label}
${description}
`; - btn.onclick = () => selectMode(mode); - return btn; -} - -function selectMode(mode) { - installMode = mode; - setStepsForMode(mode); - - const overlay = document.getElementById('welcome-overlay'); - if (overlay) overlay.classList.add('hidden'); - - // Send mode selection to server - if (ws && ws.readyState === WebSocket.OPEN) { - ws.send(JSON.stringify({ type: 'select_mode', mode: mode })); - } -} - -function scrollToBottom() { - const chat = document.getElementById('chat-messages'); - if (chat) { - // Double-RAF ensures DOM has fully rendered before scrolling - requestAnimationFrame(() => { - requestAnimationFrame(() => { - chat.scrollTop = chat.scrollHeight; - }); - }); - } -} - -// ─── Initialize ────────────────────────────────────────────────── - -document.addEventListener('DOMContentLoaded', () => { - renderSteps(); - connect(); - - // Welcome audio is played via