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:
@@ -252,7 +252,10 @@ export async function pullCommand(force = false): Promise<void> {
|
||||
|
||||
writeEndpointToDisk(ep, endpointDir);
|
||||
|
||||
const hash = computeEndpointHash(ep);
|
||||
// Хеш считаем от того, что реально записалось на диск,
|
||||
// чтобы он совпадал при последующем readEndpointFromDisk в status
|
||||
const diskEp = readEndpointFromDisk(endpointDir);
|
||||
const hash = computeEndpointHash(diskEp || ep);
|
||||
newState.endpoints[ep.id] = {
|
||||
updated_at: ep.updated_at,
|
||||
folder_path: path.relative(root, endpointDir),
|
||||
|
||||
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}\\`));
|
||||
}
|
||||
}
|
||||
34
src/index.ts
34
src/index.ts
@@ -6,13 +6,16 @@ import { initCommand } from './commands/init';
|
||||
import { pullCommand } from './commands/pull';
|
||||
import { pushCommand } from './commands/push';
|
||||
import { statusCommand } from './commands/status';
|
||||
import { updateCommand, updateApplyCommand, checkForUpdateBackground } from './commands/update';
|
||||
|
||||
const VERSION = '1.0.0';
|
||||
|
||||
const program = new Command();
|
||||
|
||||
program
|
||||
.name('kisync')
|
||||
.description('CLI tool for syncing local folders with KIS API Builder')
|
||||
.version('1.0.0');
|
||||
.version(VERSION);
|
||||
|
||||
program
|
||||
.command('init')
|
||||
@@ -20,8 +23,9 @@ program
|
||||
.action(async () => {
|
||||
try {
|
||||
await initCommand();
|
||||
await checkForUpdateBackground(VERSION);
|
||||
} catch (err: any) {
|
||||
console.error(chalk.red(`Error: ${err.message}`));
|
||||
console.error(chalk.red(`Ошибка: ${err.message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
@@ -33,8 +37,9 @@ program
|
||||
.action(async (opts) => {
|
||||
try {
|
||||
await pullCommand(opts.force);
|
||||
await checkForUpdateBackground(VERSION);
|
||||
} catch (err: any) {
|
||||
console.error(chalk.red(`Error: ${err.message}`));
|
||||
console.error(chalk.red(`Ошибка: ${err.message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
@@ -46,8 +51,9 @@ program
|
||||
.action(async (opts) => {
|
||||
try {
|
||||
await pushCommand(opts.force);
|
||||
await checkForUpdateBackground(VERSION);
|
||||
} catch (err: any) {
|
||||
console.error(chalk.red(`Error: ${err.message}`));
|
||||
console.error(chalk.red(`Ошибка: ${err.message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
@@ -58,8 +64,26 @@ program
|
||||
.action(async () => {
|
||||
try {
|
||||
await statusCommand();
|
||||
await checkForUpdateBackground(VERSION);
|
||||
} catch (err: any) {
|
||||
console.error(chalk.red(`Error: ${err.message}`));
|
||||
console.error(chalk.red(`Ошибка: ${err.message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
program
|
||||
.command('update')
|
||||
.description('Проверить обновления и обновить kisync')
|
||||
.option('--apply', 'Скачать и установить обновление')
|
||||
.action(async (opts) => {
|
||||
try {
|
||||
if (opts.apply) {
|
||||
await updateApplyCommand(VERSION);
|
||||
} else {
|
||||
await updateCommand(VERSION);
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error(chalk.red(`Ошибка: ${err.message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user