diff --git a/PAI-Install/engine/steps-fresh.ts b/PAI-Install/engine/steps-fresh.ts index a24c1165..9e32bd2a 100644 --- a/PAI-Install/engine/steps-fresh.ts +++ b/PAI-Install/engine/steps-fresh.ts @@ -11,7 +11,7 @@ import { buildOpenCodeBinary } from "./build-opencode.ts"; import type { BuildResult } from "./build-opencode.ts"; import { PROVIDER_MODELS, PROVIDER_LABELS } from "./provider-models.ts"; import type { ProviderName } from "./provider-models.ts"; -import { existsSync, mkdirSync, writeFileSync, chmodSync, symlinkSync, unlinkSync, lstatSync, realpathSync, readFileSync } from "node:fs"; +import { existsSync, mkdirSync, writeFileSync, chmodSync, symlinkSync, unlinkSync, lstatSync, realpathSync, readFileSync, renameSync } from "node:fs"; import { join, resolve, dirname } from "node:path"; import { homedir } from "node:os"; @@ -380,14 +380,19 @@ ${providerEnvVar}=${state.collected.apiKey || ""} } // If it already points to our location, nothing to do } else if (stats.isDirectory()) { - // It's a real directory - backup and replace with symlink + if (realpathSync(globalOpencodeLink) === localOpencodeDir) { + console.warn("PAI install directory already matches ~/.opencode; no symlink changes needed."); + } else { + // It's a real directory — back it up and replace it so validation reads the new install. const backupPath = `${globalOpencodeLink}.backup-${Date.now()}`; - // Note: In production, this would need proper backup logic - // For now, we just warn and don't overwrite - throw new Error( - `~/.opencode is a directory (not a symlink). ` + - `Please backup and remove it manually, then re-run the installer.` - ); + renameSync(globalOpencodeLink, backupPath); + symlinkSync(localOpencodeDir, globalOpencodeLink, "dir"); + console.warn(`Warning: Existing ~/.opencode directory moved to ${backupPath}`); + } + } else { + // Regular file (not a symlink or directory) — replace it with the symlink. + unlinkSync(globalOpencodeLink); + symlinkSync(localOpencodeDir, globalOpencodeLink, "dir"); } } else { // No ~/.opencode exists - create symlink @@ -585,4 +590,4 @@ export async function runFreshInstall( import { exec as execCallback } from "node:child_process"; import { promisify } from "node:util"; -const exec = promisify(execCallback); \ No newline at end of file +const exec = promisify(execCallback); diff --git a/PAI-Install/public/app.js b/PAI-Install/public/app.js index 2fb705f9..b3b83f82 100644 --- a/PAI-Install/public/app.js +++ b/PAI-Install/public/app.js @@ -12,10 +12,12 @@ 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`); @@ -35,8 +37,10 @@ function connect() { ws.onclose = () => { connected = false; - addMessage('system', 'Connection lost. Reconnecting...', false); - setTimeout(connect, 2000); // Auto-reconnect + if (!installationComplete) { + addMessage('system', 'Connection lost. Reconnecting...', false); + setTimeout(connect, 2000); // Auto-reconnect + } }; ws.onerror = () => { @@ -59,6 +63,7 @@ function handleServerMessage(msg) { break; case 'mode_selected': + installationComplete = false; setStepsForMode(msg.mode); renderSteps(); break; @@ -111,6 +116,7 @@ function handleServerMessage(msg) { break; case 'install_complete': + installationComplete = true; renderSummary(msg.summary); break;