import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; import chalk from 'chalk'; import fetch from 'node-fetch'; const GITEA_API = 'https://gitea.esh-service.ru/api/v1'; const REPO_OWNER = 'public'; const REPO_NAME = 'api_builder_cli_client'; const EXE_NAME = 'kisync.exe'; interface Release { id: number; tag_name: string; name: string; body: string; published_at: string; assets: { id: number; name: string; size: number; browser_download_url: string; }[]; } function parseVersion(tag: string): number[] { return tag.replace(/^v/, '').split('.').map(Number); } function isNewer(remote: string, local: string): boolean { const r = parseVersion(remote); const l = parseVersion(local); for (let i = 0; i < Math.max(r.length, l.length); i++) { const rv = r[i] || 0; const lv = l[i] || 0; if (rv > lv) return true; if (rv < lv) return false; } return false; } async function fetchReleases(): Promise { const url = `${GITEA_API}/repos/${REPO_OWNER}/${REPO_NAME}/releases`; const res = await fetch(url, { timeout: 5000 }); if (!res.ok) { throw new Error(`HTTP ${res.status}`); } return res.json() as Promise; } /** * Фоновая проверка обновлений — вызывается при каждой команде. * Не блокирует работу, просто выводит подсказку в конце. */ export async function checkForUpdateBackground(currentVersion: string): Promise { try { const releases = await fetchReleases(); const latest = releases.find(r => r.assets.some(a => a.name.toLowerCase() === EXE_NAME.toLowerCase()) ); if (latest && isNewer(latest.tag_name, currentVersion)) { console.log(''); console.log( chalk.yellow(` Доступна новая версия: ${latest.tag_name} (текущая: v${currentVersion})`) ); console.log( chalk.yellow(` Обновить: kisync update --apply`) ); } } catch { // Тихо игнорируем — фоновая проверка не должна мешать работе } } /** * kisync update — показать список релизов */ export async function updateCommand(currentVersion: string): Promise { console.log(chalk.gray('Проверка обновлений...')); console.log(chalk.gray(`Текущая версия: v${currentVersion}\n`)); let releases: Release[]; try { releases = await fetchReleases(); } catch (err: any) { throw new Error(`Не удалось подключиться к серверу обновлений: ${err.message}`); } if (releases.length === 0) { console.log(chalk.green('Релизы не найдены.')); return; } console.log(chalk.bold('Доступные релизы:\n')); for (const rel of releases) { const tag = rel.tag_name; const date = new Date(rel.published_at).toLocaleDateString('ru-RU'); const isCurrent = tag === `v${currentVersion}` || tag === currentVersion; const marker = isCurrent ? chalk.green(' ← текущая') : ''; const newer = isNewer(tag, currentVersion); const prefix = newer ? chalk.cyan(' ↑') : chalk.gray(' '); console.log(`${prefix} ${chalk.bold(tag)} ${chalk.gray(date)}${marker}`); if (rel.body) { const lines = rel.body.trim().split('\n').slice(0, 3); for (const line of lines) { console.log(chalk.gray(` ${line}`)); } } if (rel.assets.length > 0) { for (const asset of rel.assets) { const sizeMb = (asset.size / (1024 * 1024)).toFixed(1); console.log(chalk.gray(` ${asset.name} (${sizeMb} МБ)`)); } } console.log(''); } // Найти последний релиз с exe const latest = releases.find(r => r.assets.some(a => a.name.toLowerCase() === EXE_NAME.toLowerCase()) ); if (!latest) { console.log(chalk.yellow('Ни один релиз не содержит kisync.exe.')); return; } if (!isNewer(latest.tag_name, currentVersion)) { console.log(chalk.green('У вас установлена последняя версия.')); return; } console.log( chalk.cyan(`Новая версия: ${latest.tag_name} (текущая: v${currentVersion})`) ); console.log( chalk.gray(`Для обновления выполните: kisync update --apply\n`) ); } /** * kisync update --apply — скачать и заменить exe */ export async function updateApplyCommand(currentVersion: string): Promise { console.log(chalk.gray('Проверка обновлений...')); let releases: Release[]; try { releases = await fetchReleases(); } catch (err: any) { throw new Error(`Не удалось подключиться к серверу обновлений: ${err.message}`); } const latest = releases.find(r => r.assets.some(a => a.name.toLowerCase() === EXE_NAME.toLowerCase()) ); if (!latest) { throw new Error('Ни один релиз не содержит kisync.exe.'); } if (!isNewer(latest.tag_name, currentVersion)) { console.log(chalk.green(`Уже установлена последняя версия (v${currentVersion}).`)); return; } const asset = latest.assets.find( a => a.name.toLowerCase() === EXE_NAME.toLowerCase() )!; console.log(chalk.cyan(`Скачивание ${latest.tag_name}...`)); console.log(chalk.gray(` ${asset.browser_download_url}`)); const res = await fetch(asset.browser_download_url); if (!res.ok) { throw new Error(`Ошибка загрузки: HTTP ${res.status}`); } const buffer = await res.buffer(); console.log(chalk.gray(` Загружено ${(buffer.length / (1024 * 1024)).toFixed(1)} МБ`)); // Путь к текущему exe const currentExe = process.execPath; const exeDir = path.dirname(currentExe); const exeName = path.basename(currentExe); // На Windows нельзя перезаписать работающий exe — переименуем в .old const oldExe = path.join(exeDir, `${exeName}.old`); const newExe = path.join(exeDir, exeName); try { if (fs.existsSync(oldExe)) { fs.unlinkSync(oldExe); } fs.renameSync(currentExe, oldExe); fs.writeFileSync(newExe, buffer); console.log(chalk.green(`\nОбновлено до ${latest.tag_name}!`)); console.log(chalk.gray(` Резервная копия: ${oldExe}`)); console.log(chalk.gray(` Перезапустите kisync для использования новой версии.`)); } catch (err: any) { // Попытка восстановить при ошибке if (fs.existsSync(oldExe) && !fs.existsSync(newExe)) { try { fs.renameSync(oldExe, newExe); } catch {} } // Запасной вариант — сохранить во временную папку const tmpPath = path.join(os.tmpdir(), EXE_NAME); fs.writeFileSync(tmpPath, buffer); console.log(chalk.yellow(`\nНе удалось заменить exe: ${err.message}`)); console.log(chalk.yellow(`Новая версия сохранена: ${tmpPath}`)); console.log(chalk.yellow(`Скопируйте вручную в: ${exeDir}\\`)); } }