phion
Version:
Phion Development Agent and Vite Plugin for seamless code sync and auto-deploy
943 lines (939 loc) • 29 kB
JavaScript
// src/vscode-utils.ts
import { exec } from "child_process";
import fs from "fs";
import http from "http";
import os from "os";
import path from "path";
import { promisify } from "util";
var execAsync = promisify(exec);
var globalBrowserOpened = false;
var BROWSER_OPENED_FLAG_FILE = path.join(os.tmpdir(), "phion-browser-opened.flag");
function detectVSCode() {
return !!(process.env.VSCODE_PID || process.env.TERM_PROGRAM === "vscode" || process.env.VSCODE_INJECTION === "1" || process.env.TERM_PROGRAM === "cursor" || process.env.CURSOR_PID || process.env.CURSOR_TRACE_ID);
}
function isCursor() {
return !!(process.env.CURSOR_TRACE_ID || process.env.VSCODE_GIT_ASKPASS_NODE?.includes("Cursor.app"));
}
async function isCodeCommandAvailable() {
const useCursor = isCursor();
const command = useCursor ? "cursor" : "code";
try {
await execAsync(`${command} --version`);
return true;
} catch {
try {
const altCommand = useCursor ? "code" : "cursor";
await execAsync(`${altCommand} --version`);
return true;
} catch {
return false;
}
}
}
function checkDevServerReady(port) {
return new Promise((resolve) => {
const req = http.get(`http://localhost:${port}`, (res) => {
resolve(res.statusCode === 200);
});
req.on("error", () => {
resolve(false);
});
req.setTimeout(1e3, () => {
req.destroy();
resolve(false);
});
});
}
async function findVitePort() {
const commonPorts = [3e3, 3001, 5173, 5174, 5175, 5176, 5177];
for (const port of commonPorts) {
if (await checkDevServerReady(port)) {
return port;
}
}
return null;
}
async function openInVSCodeSimpleBrowser(url) {
const useCursor = isCursor();
const command = useCursor ? "cursor" : "code";
try {
if (useCursor) {
await execAsync(`${command} --command "vscode.open" --command-args "${url}"`);
return true;
} else {
await execAsync(`${command} --command "simpleBrowser.show" --command-args "${url}"`);
return true;
}
} catch {
try {
await execAsync(`${command} --command "simpleBrowser.show" --command-args "${url}"`);
return true;
} catch {
try {
const encodedUrl = encodeURIComponent(url);
await execAsync(
`${command} --open-url "vscode://ms-vscode.vscode-simple-browser/show?url=${encodedUrl}"`
);
return true;
} catch {
try {
await execAsync(`${command} --open-url "${url}"`);
return true;
} catch {
return false;
}
}
}
}
}
async function openInSystemBrowser(url) {
try {
const platform = process.platform;
let command;
switch (platform) {
case "darwin":
command = `open "${url}"`;
break;
case "win32":
command = `start "${url}"`;
break;
default:
command = `xdg-open "${url}"`;
break;
}
await execAsync(command);
return true;
} catch {
return false;
}
}
function isBrowserAlreadyOpened() {
return globalBrowserOpened || fs.existsSync(BROWSER_OPENED_FLAG_FILE);
}
function markBrowserAsOpened() {
globalBrowserOpened = true;
try {
fs.writeFileSync(BROWSER_OPENED_FLAG_FILE, Date.now().toString());
} catch (error) {
}
}
async function openPreview(config, debug = false) {
if (!config.autoOpen) {
return;
}
if (isBrowserAlreadyOpened()) {
if (debug) {
console.log("\u{1F6AB} Browser already opened, skipping...");
}
return;
}
let port = config.port;
let url = config.url;
if (!url) {
const foundPort = await findVitePort();
if (foundPort) {
port = foundPort;
url = `http://localhost:${port}`;
} else {
url = `http://localhost:${port}`;
}
}
const isReady = await checkDevServerReady(port);
if (!isReady) {
if (debug) {
console.warn(`Dev server not ready at ${url}`);
}
return;
}
if (debug) {
console.log(`\u{1F680} Development server ready: ${url}`);
}
const isVSCode = detectVSCode();
const hasCodeCommand = await isCodeCommandAvailable();
if (isVSCode && hasCodeCommand) {
const success = await openInVSCodeSimpleBrowser(url);
if (success) {
markBrowserAsOpened();
if (debug) {
console.log("\u2705 Preview opened in browser");
}
return;
}
}
const systemSuccess = await openInSystemBrowser(url);
if (systemSuccess) {
markBrowserAsOpened();
if (debug) {
console.log("\u2705 Preview opened in browser");
}
return;
}
console.log(`\u{1F4A1} Open manually: ${url}`);
}
// src/agent.ts
import { exec as exec2 } from "child_process";
import chokidar from "chokidar";
import crypto from "crypto";
import fs2 from "fs";
import http2 from "http";
import path2 from "path";
import { io } from "socket.io-client";
import { promisify as promisify2 } from "util";
var execAsync2 = promisify2(exec2);
var PhionAgent = class {
socket = null;
watcher = null;
envWatcher = null;
// Отдельный watcher для .env файлов
httpServer = null;
isConnected = false;
isGitRepo = false;
config;
gitOperationCooldown = false;
// Новое поле для предотвращения ложных событий
constructor(config) {
this.config = config;
}
async start() {
console.log("\u{1F680} Phion Agent");
if (this.config.debug) {
console.log(`\u{1F4E1} Connecting to: ${this.config.wsUrl}`);
console.log(`\u{1F194} Project ID: ${this.config.projectId}`);
}
await this.startLocalServer();
await this.checkGitRepository();
await this.connectWebSocket();
this.startFileWatcher();
console.log("\u2705 Agent running - edit files to sync changes");
if (this.config.debug) {
console.log("\u{1F310} Local command server: http://localhost:3333");
}
console.log("Press Ctrl+C to stop");
}
async startLocalServer() {
return new Promise((resolve, reject) => {
this.httpServer = http2.createServer((req, res) => {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
if (req.method === "OPTIONS") {
res.writeHead(200);
res.end();
return;
}
if (req.method === "POST" && req.url === "/open-url") {
let body = "";
req.on("data", (chunk) => {
body += chunk.toString();
});
req.on("end", async () => {
try {
const { url } = JSON.parse(body);
const success = await openInSystemBrowser(url);
res.writeHead(200, { "Content-Type": "application/json" });
res.end(
JSON.stringify({
success,
message: success ? "URL opened successfully" : "Failed to open URL"
})
);
} catch (error) {
if (this.config.debug) {
console.error("\u274C Local server: Error opening URL:", error);
}
res.writeHead(500, { "Content-Type": "application/json" });
res.end(
JSON.stringify({
success: false,
error: error.message
})
);
}
});
return;
}
if (req.method === "GET" && req.url === "/status") {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(
JSON.stringify({
status: "running",
projectId: this.config.projectId,
connected: this.isConnected
})
);
return;
}
res.writeHead(404, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Not found" }));
});
this.httpServer.listen(3333, "localhost", () => {
if (this.config.debug) {
console.log("\u{1F310} Local command server started on http://localhost:3333");
}
resolve();
});
this.httpServer.on("error", (error) => {
if (error.code === "EADDRINUSE") {
if (this.config.debug) {
console.log("\u26A0\uFE0F Port 3333 already in use, trying 3334...");
}
this.httpServer?.listen(3334, "localhost", () => {
if (this.config.debug) {
console.log("\u{1F310} Local command server started on http://localhost:3334");
}
resolve();
});
} else {
console.error("\u274C Failed to start local server:", error);
reject(error);
}
});
});
}
async checkGitRepository() {
try {
await execAsync2("git rev-parse --git-dir");
this.isGitRepo = true;
if (this.config.debug) {
console.log("\u2705 Git repository detected");
try {
const { stdout } = await execAsync2("git remote get-url origin");
console.log(`\u{1F517} Remote origin: ${stdout.trim()}`);
} catch (error) {
console.log("\u26A0\uFE0F No remote origin configured");
}
}
} catch (error) {
if (this.config.debug) {
console.log("\u26A0\uFE0F Not a git repository - initializing...");
}
await this.initializeGitRepository();
}
}
async initializeGitRepository() {
try {
if (this.config.debug) {
console.log("\u{1F527} Initializing git repository...");
}
await execAsync2("git init");
const repoUrl = `https://github.com/phion-dev/phion-project-${this.config.projectId}.git`;
await execAsync2(`git remote add origin ${repoUrl}`);
try {
await execAsync2("git add .");
await execAsync2('git commit -m "Initial commit from Phion template"');
} catch (commitError) {
}
this.isGitRepo = true;
if (this.config.debug) {
console.log("\u2705 Git repository setup completed");
}
} catch (error) {
console.error("\u274C Failed to initialize git repository:", error.message);
this.isGitRepo = false;
if (this.config.debug) {
console.log("\u26A0\uFE0F Git commands will be disabled");
}
}
}
async connectWebSocket() {
return new Promise((resolve) => {
this.socket = io(this.config.wsUrl, {
transports: ["websocket", "polling"],
// Support both transports for reliability
timeout: 3e4,
// 30 seconds - increased from 10 seconds for production
reconnection: true,
reconnectionAttempts: 5,
reconnectionDelay: 2e3,
reconnectionDelayMax: 1e4,
// Max 10 seconds between attempts
randomizationFactor: 0.5,
// Add jitter to reconnection attempts
// 🚀 PRODUCTION SETTINGS - match server configuration
upgrade: true,
rememberUpgrade: true,
// Enable connection state recovery
auth: {
projectId: this.config.projectId
// Include projectId in handshake
}
});
this.socket.on("connect", () => {
if (this.config.debug) {
console.log("\u2705 Connected to Phion");
}
this.socket.emit("authenticate", {
projectId: this.config.projectId,
clientType: "agent"
});
});
this.socket.on("authenticated", (data) => {
if (this.config.debug) {
console.log(`\u{1F510} Authenticated for project: ${data.projectId}`);
}
this.isConnected = true;
this.openPreviewIfEnabled();
resolve();
});
setTimeout(() => {
if (!this.isConnected) {
if (this.config.debug) {
console.log("\u23F0 Connection timeout, but continuing anyway...");
}
resolve();
}
}, 15e3);
this.setupEventHandlers();
});
}
setupEventHandlers() {
if (!this.socket) return;
this.socket.onAny((eventName, ...args) => {
if (this.config.debug) {
console.log(`\u{1F4E1} [Agent] Received event: ${eventName}`, args.length > 0 ? args[0] : "");
}
});
this.socket.on("file_saved", (data) => {
if (this.config.debug) {
console.log(`\u{1F4BE} File saved: ${data.filePath}`);
}
});
this.socket.on("file_updated", (data) => {
if (this.config.debug) {
console.log(`\u{1F504} File updated by another client: ${data.filePath}`);
}
});
this.socket.on("discard_local_changes", async (data) => {
if (this.config.debug) {
console.log("\u{1F504} [AGENT] Received discard_local_changes command from server");
console.log("\u{1F504} Discarding local changes...");
}
await this.discardLocalChanges();
});
this.socket.on("git_pull_with_token", async (data) => {
if (this.config.debug) {
console.log("\u{1F4E5} [AGENT] Received git_pull_with_token command from server");
console.log("\u{1F4E5} Syncing with latest changes...");
}
await this.gitPullWithToken(data.token, data.repoUrl);
});
this.socket.on("update_local_files", async (data) => {
if (this.config.debug) {
console.log("\u{1F4C4} [AGENT] Received update_local_files command from server");
console.log("\u{1F4C4} Updating local files...");
}
await this.updateLocalFiles(data.files);
});
this.socket.on("save_success", (data) => {
if (this.config.debug) {
console.log("\u{1F4BE} [AGENT] Save operation completed successfully");
}
});
this.socket.on("discard_success", (data) => {
if (this.config.debug) {
console.log("\u{1F504} [AGENT] Discard operation completed successfully");
}
});
this.socket.on("error", (error) => {
console.error("\u274C WebSocket error:", error.message);
});
this.socket.on("disconnect", (reason) => {
if (this.config.debug) {
console.log(`\u274C Disconnected: ${reason}`);
}
this.isConnected = false;
const serverInitiated = ["io server disconnect", "server namespace disconnect"];
const networkIssues = ["ping timeout", "transport close", "transport error"];
const clientInitiated = ["io client disconnect", "client namespace disconnect"];
if (serverInitiated.includes(reason)) {
if (this.config.debug) {
console.log("\u{1F504} Server-initiated disconnect, will attempt reconnection");
}
} else if (networkIssues.includes(reason)) {
if (this.config.debug) {
console.log("\u26A0\uFE0F Network issue detected, checking connection quality");
}
} else if (clientInitiated.includes(reason)) {
if (this.config.debug) {
console.log("\u{1F44B} Client-initiated disconnect, normal closure");
}
return;
}
if (!clientInitiated.includes(reason)) {
setTimeout(() => {
if (this.config.debug) {
console.log("\u{1F504} Attempting to reconnect...");
}
this.socket?.connect();
}, 5e3);
}
});
this.socket.on("connect_error", (error) => {
if (this.config.debug) {
console.error("\u274C Connection failed:", error.message);
console.log("\u{1F504} Will retry connection...");
}
});
}
async discardLocalChanges() {
if (!this.isGitRepo) {
if (this.config.debug) {
console.log("\u26A0\uFE0F Not a git repository - cannot discard changes");
}
this.socket?.emit("git_command_result", {
projectId: this.config.projectId,
command: "discard",
success: false,
error: "Not a git repository"
});
return;
}
try {
if (this.watcher) {
this.watcher.close();
this.watcher = null;
}
await execAsync2("git reset --hard HEAD");
await execAsync2("git clean -fd");
this.socket?.emit("git_command_result", {
projectId: this.config.projectId,
command: "discard",
success: true
});
this.gitOperationCooldown = true;
if (this.config.debug) {
console.log("\u2705 Changes discarded");
console.log("\u{1F504} Git operation cooldown started (5s)");
}
this.startFileWatcher();
setTimeout(() => {
this.gitOperationCooldown = false;
if (this.config.debug) {
console.log("\u{1F504} Git operation cooldown ended");
}
}, 5e3);
} catch (error) {
console.error("\u274C Error discarding changes:", error.message);
this.socket?.emit("git_command_result", {
projectId: this.config.projectId,
command: "discard",
success: false,
error: error.message
});
this.gitOperationCooldown = false;
this.startFileWatcher();
}
}
async gitPullWithToken(token, repoUrl) {
if (!this.isGitRepo) {
if (this.config.debug) {
console.log("\u26A0\uFE0F Not a git repository - cannot pull");
}
this.socket?.emit("git_command_result", {
projectId: this.config.projectId,
command: "pull",
success: false,
error: "Not a git repository"
});
return;
}
try {
if (this.watcher) {
this.watcher.close();
this.watcher = null;
}
const authenticatedUrl = repoUrl.replace(
"https://github.com/",
`https://x-access-token:${token}@github.com/`
);
await execAsync2(`git fetch ${authenticatedUrl} main`);
await execAsync2(`git reset --hard FETCH_HEAD`);
if (this.config.debug) {
console.log("\u2705 Synced with latest changes");
}
this.socket?.emit("git_command_result", {
projectId: this.config.projectId,
command: "pull",
success: true
});
this.gitOperationCooldown = true;
if (this.config.debug) {
console.log("\u{1F504} Git operation cooldown started (5s)");
}
this.startFileWatcher();
setTimeout(() => {
this.gitOperationCooldown = false;
if (this.config.debug) {
console.log("\u{1F504} Git operation cooldown ended");
}
}, 5e3);
} catch (error) {
console.error("\u274C Error syncing:", error.message);
this.socket?.emit("git_command_result", {
projectId: this.config.projectId,
command: "pull",
success: false,
error: error.message
});
this.gitOperationCooldown = false;
this.startFileWatcher();
}
}
async updateLocalFiles(files) {
try {
if (this.watcher) {
this.watcher.close();
this.watcher = null;
}
for (const file of files) {
try {
const dir = path2.dirname(file.path);
if (!fs2.existsSync(dir)) {
fs2.mkdirSync(dir, { recursive: true });
}
fs2.writeFileSync(file.path, file.content, "utf8");
if (this.config.debug) {
console.log(`\u2705 Updated: ${file.path}`);
}
} catch (fileError) {
console.error(`\u274C Error updating file ${file.path}:`, fileError.message);
}
}
this.socket?.emit("git_command_result", {
projectId: this.config.projectId,
command: "update_files",
success: true
});
this.startFileWatcher();
if (this.config.debug) {
console.log("\u2705 Files updated");
}
} catch (error) {
console.error("\u274C Error updating files:", error.message);
this.socket?.emit("git_command_result", {
projectId: this.config.projectId,
command: "update_files",
success: false,
error: error.message
});
this.startFileWatcher();
}
}
startFileWatcher() {
if (this.watcher) {
return;
}
if (this.config.debug) {
console.log("\u{1F440} Watching for file changes...");
}
this.watcher = chokidar.watch(".", {
ignored: [
"node_modules/**",
".git/**",
"dist/**",
"build/**",
".next/**",
".turbo/**",
"*.log",
"phion.js",
".env*",
"*.timestamp-*.mjs",
"vite.config.*.timestamp-*.mjs",
"**/*timestamp-*",
"**/*.timestamp-*.*",
"*.tmp",
"*.temp",
".vite/**"
],
ignoreInitial: true,
persistent: true
});
this.watcher.on("change", (filePath) => {
this.handleFileChange(filePath);
});
this.watcher.on("add", (filePath) => {
this.handleFileChange(filePath);
});
this.watcher.on("unlink", (filePath) => {
this.handleFileDelete(filePath);
});
this.watcher.on("error", (error) => {
console.error("\u274C File watcher error:", error);
});
this.startEnvWatcher();
}
startEnvWatcher() {
if (this.envWatcher) {
return;
}
if (this.config.debug) {
console.log("\u{1F510} Watching for .env file changes...");
}
this.envWatcher = chokidar.watch(
[
".env",
".env.local",
".env.development",
".env.production",
".env.development.local",
".env.production.local",
".env.test",
".env.test.local"
],
{
ignoreInitial: true,
persistent: true
}
);
this.envWatcher.on("change", (filePath) => {
this.handleEnvFileChange(filePath);
});
this.envWatcher.on("add", (filePath) => {
this.handleEnvFileChange(filePath);
});
this.envWatcher.on("unlink", (filePath) => {
this.handleEnvFileDelete(filePath);
});
this.envWatcher.on("error", (error) => {
console.error("\u274C Env watcher error:", error);
});
}
async handleFileChange(filePath) {
if (!this.isConnected) {
if (this.config.debug) {
console.log(`\u23F3 Not connected, skipping: ${filePath}`);
}
return;
}
if (this.gitOperationCooldown) {
if (this.config.debug) {
console.log(`\u{1F504} Git operation in progress, skipping file change: ${filePath}`);
}
return;
}
if (filePath.includes(".timestamp-") || filePath.includes("timestamp-")) {
if (this.config.debug) {
console.log(`\u23ED\uFE0F Ignoring timestamp file: ${filePath}`);
}
return;
}
try {
const content = fs2.readFileSync(filePath, "utf-8");
const hash = crypto.createHash("sha256").update(content).digest("hex");
if (this.config.debug) {
console.log(`\u{1F4DD} Syncing: ${filePath}`);
}
const fileChange = {
projectId: this.config.projectId,
filePath: filePath.replace(/\\/g, "/"),
content,
hash,
timestamp: Date.now()
};
this.socket?.emit("file_change", fileChange);
} catch (error) {
console.error(`\u274C Error reading file ${filePath}:`, error.message);
}
}
handleFileDelete(filePath) {
if (!this.isConnected) {
if (this.config.debug) {
console.log(`\u23F3 Queuing deletion: ${filePath} (not connected)`);
}
return;
}
if (this.config.debug) {
console.log(`\u{1F5D1}\uFE0F Deleted: ${filePath}`);
}
const fileDelete = {
projectId: this.config.projectId,
filePath: filePath.replace(/\\/g, "/"),
timestamp: Date.now()
};
this.socket?.emit("file_delete", fileDelete);
}
async handleEnvFileChange(filePath) {
if (!this.isConnected) {
if (this.config.debug) {
console.log(`\u23F3 Not connected, skipping env file: ${filePath}`);
}
return;
}
try {
const content = fs2.readFileSync(filePath, "utf-8");
if (this.config.debug) {
console.log(`\u{1F510} Syncing env file: ${filePath}`);
}
const envFileChange = {
projectId: this.config.projectId,
filePath: filePath.replace(/\\/g, "/"),
content,
timestamp: Date.now()
};
this.socket?.emit("env_file_change", envFileChange);
} catch (error) {
console.error(`\u274C Error reading env file ${filePath}:`, error.message);
}
}
handleEnvFileDelete(filePath) {
if (!this.isConnected) {
if (this.config.debug) {
console.log(`\u23F3 Not connected, skipping env file deletion: ${filePath}`);
}
return;
}
if (this.config.debug) {
console.log(`\u{1F5D1}\uFE0F Env file deleted: ${filePath}`);
}
this.socket?.emit("env_file_delete", {
projectId: this.config.projectId,
filePath: filePath.replace(/\\/g, "/"),
timestamp: Date.now()
});
}
async openPreviewIfEnabled() {
if (this.config.debug) {
console.log("\u{1F50D} Checking if preview should be opened...");
console.log("\u{1F4CB} Toolbar config:", JSON.stringify(this.config.toolbar, null, 2));
}
const toolbarConfig = this.config.toolbar;
if (!toolbarConfig?.enabled) {
if (this.config.debug) {
console.log("\u23ED\uFE0F Toolbar disabled, skipping preview");
}
return;
}
if (!toolbarConfig?.autoOpen) {
if (this.config.debug) {
console.log("\u23ED\uFE0F Auto-open disabled, skipping preview");
}
return;
}
if (this.config.debug) {
console.log("\u2705 Preview will be opened in 3 seconds...");
}
const vsCodeConfig = {
autoOpen: true,
port: 5173
// Vite default port
};
setTimeout(async () => {
if (this.config.debug) {
console.log("\u{1F680} Opening preview now...");
}
try {
await openPreview(vsCodeConfig, this.config.debug);
} catch (error) {
if (this.config.debug) {
console.log("\u26A0\uFE0F Failed to open preview from agent:", error.message);
}
}
}, 3e3);
}
stop() {
console.log("\u{1F6D1} Stopping agent...");
if (this.watcher) {
this.watcher.close();
this.watcher = null;
}
if (this.envWatcher) {
this.envWatcher.close();
this.envWatcher = null;
}
if (this.socket) {
this.socket.disconnect();
}
if (this.httpServer) {
this.httpServer.close();
this.httpServer = null;
}
console.log("\u2705 Stopped");
}
};
// src/version-checker.ts
import fs3 from "fs";
import path3 from "path";
import { fileURLToPath } from "url";
var __filename = fileURLToPath(import.meta.url);
var __dirname = path3.dirname(__filename);
function getCurrentVersion() {
try {
const possiblePaths = [
path3.join(__dirname, "..", "package.json"),
// Из dist/ в корень пакета
path3.join(__dirname, "..", "..", "package.json"),
// Если dist в подпапке
path3.join(process.cwd(), "node_modules", "phion", "package.json")
// В node_modules
];
for (const packageJsonPath of possiblePaths) {
try {
if (fs3.existsSync(packageJsonPath)) {
const packageJson = JSON.parse(fs3.readFileSync(packageJsonPath, "utf8"));
if (packageJson.name === "phion" && packageJson.version) {
return packageJson.version;
}
}
} catch (pathError) {
continue;
}
}
return "0.0.1";
} catch (error) {
return "0.0.1";
}
}
async function checkLatestVersion(wsUrl) {
try {
const httpUrl = wsUrl.replace("ws://", "http://").replace("wss://", "https://");
const versionUrl = `${httpUrl}/api/version`;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5e3);
const response = await fetch(versionUrl, {
method: "GET",
signal: controller.signal
});
clearTimeout(timeoutId);
if (response.ok) {
const data = await response.json();
return data.latestAgentVersion || null;
}
} catch (error) {
if (process.env.DEBUG) {
console.debug("Failed to check latest version:", error);
}
}
return null;
}
function isNewerVersion(latest, current) {
try {
const latestParts = latest.split(".").map(Number);
const currentParts = current.split(".").map(Number);
for (let i = 0; i < Math.max(latestParts.length, currentParts.length); i++) {
const latestPart = latestParts[i] || 0;
const currentPart = currentParts[i] || 0;
if (latestPart > currentPart) return true;
if (latestPart < currentPart) return false;
}
return false;
} catch (error) {
return false;
}
}
async function checkForUpdates(wsUrl) {
const current = getCurrentVersion();
const latest = await checkLatestVersion(wsUrl);
const hasUpdate = latest ? isNewerVersion(latest, current) : false;
return {
current,
latest: latest || void 0,
hasUpdate
};
}
export {
detectVSCode,
openPreview,
PhionAgent,
getCurrentVersion,
checkForUpdates
};