Add self-update command and fix hash mismatch after pull
- kisync update: show available releases from Gitea - kisync update --apply: download and replace exe in-place - Background update check after every command (non-blocking) - Fix: compute hash from disk-read object (not server object) to prevent false "modified" status after pull due to line-ending differences Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
218
src/commands/update.ts
Normal file
218
src/commands/update.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
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<Release[]> {
|
||||
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<Release[]>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Фоновая проверка обновлений — вызывается при каждой команде.
|
||||
* Не блокирует работу, просто выводит подсказку в конце.
|
||||
*/
|
||||
export async function checkForUpdateBackground(currentVersion: string): Promise<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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}\\`));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user