4 Commits
v1.0.0 ... main

Author SHA1 Message Date
5d6e7bbe56 Bump version to 1.1.0
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 17:44:23 +03:00
0ad43a6981 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>
2026-03-14 17:41:35 +03:00
9f041c2d3d Add CONTEXT.md generation on kisync init
Creates a comprehensive context file for AI assistants (Claude Code, Codex)
that explains the project structure, script execution model, execQuery usage,
query file naming conventions, and available sandbox globals.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 17:08:08 +03:00
302c7ab9f5 Add self-downloading installer and README
- install.bat downloads exe from Gitea release or uses local copy
- Adds to user PATH automatically
- README with full usage guide, file structure, conflict resolution

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 16:36:09 +03:00
7 changed files with 644 additions and 34 deletions

161
README.md Normal file
View File

@@ -0,0 +1,161 @@
# kisync — KIS API Builder Sync CLI
CLI-клиент для двусторонней синхронизации локальных файлов с сервером [KIS API Builder](https://gitea.esh-service.ru/public/api_builder_cli_client).
## Установка
### Вариант 1: Автоматическая (Windows)
Скачать и запустить [`install.bat`](https://gitea.esh-service.ru/public/api_builder_cli_client/releases/download/v1.0.0/install.bat) — он сам скачает `kisync.exe`, установит в `%LOCALAPPDATA%\kisync\` и добавит в PATH.
### Вариант 2: Ручная
1. Скачать `kisync.exe` со [страницы релиза](https://gitea.esh-service.ru/public/api_builder_cli_client/releases/tag/v1.0.0)
2. Положить в любую папку, которая есть в PATH (например `C:\Users\<user>\AppData\Local\kisync\`)
3. Проверить: `kisync --help`
---
## Быстрый старт
```bash
# 1. Создать папку проекта
mkdir my-api-endpoints
cd my-api-endpoints
# 2. Подключиться к серверу API Builder
kisync init
# Server URL: http://your-server:3000
# Username: admin
# Password: ****
# 3. Скачать все эндпоинты с сервера
kisync pull
# 4. Редактировать файлы (запросы, скрипты)
# ... правишь query.sql, main.js, request.http ...
# 5. Проверить что изменилось
kisync status
# 6. Отправить изменения на сервер
kisync push
```
---
## Команды
| Команда | Описание |
|---------|----------|
| `kisync init` | Подключиться к серверу (ввести URL, логин, пароль) |
| `kisync pull` | Скачать эндпоинты с сервера в локальные файлы |
| `kisync pull --force` | Перезаписать локальные изменения версией с сервера |
| `kisync push` | Загрузить локальные изменения на сервер |
| `kisync push --force` | Принудительно перезаписать сервер, игнорируя конфликты |
| `kisync status` | Показать что изменилось локально и на сервере |
---
## Структура файлов
После `kisync pull` в папке появится такая структура:
```
my-api-endpoints/
├── .kisync.json # Конфиг (host, token) — НЕ коммитить!
├── .kisync-state.json # Состояние синхронизации — НЕ коммитить!
├── Пользователи/ # Папка (Folder в API Builder)
│ ├── _folder.json # Метаданные папки
│ │
│ ├── Получить список/ # Эндпоинт (SQL)
│ │ ├── endpoint.json # Настройки: method, path, parameters...
│ │ └── query.sql # SQL-запрос
│ │
│ ├── Создать/ # Эндпоинт (Script)
│ │ ├── endpoint.json
│ │ ├── main.js # JavaScript код (или main.py)
│ │ └── queries/ # Запросы, используемые в скрипте
│ │ ├── _index.json # Индекс запросов (имена, БД)
│ │ ├── insert.sql # SQL-запрос
│ │ └── notify.http # HTTP-запрос (AQL)
│ │
│ └── Внешний сервис/ # Эндпоинт (AQL / HTTP)
│ ├── endpoint.json
│ └── request.http # HTTP-запрос
└── _no_folder/ # Эндпоинты без папки
└── ...
```
### Файлы по типу эндпоинта
| Тип | Файлы |
|-----|-------|
| **SQL** | `endpoint.json` + `query.sql` |
| **Script** | `endpoint.json` + `main.js`/`main.py` + `queries/*.sql` |
| **AQL (HTTP)** | `endpoint.json` + `request.http` |
### Формат `request.http`
```http
POST /api/v1/patients
Content-Type: application/json
{
"name": "{{name}}",
"age": {{age}}
}
```
---
## Конфликты
При `push` и `pull` kisync проверяет, не менял ли кто-то эндпоинт на сервере с момента последней синхронизации.
```
$ kisync push
! CONFLICT: Пользователи/Создать
server updated: 14.03.2026, 15:42:31
your base: 14.03.2026, 12:00:00
Use "kisync push --force" to overwrite server changes.
Or run "kisync pull --force" to get the latest version first.
```
**Варианты решения:**
- `kisync push --force` — перезаписать сервер своей версией
- `kisync pull --force` — забрать серверную версию (потерять свои правки)
- Сделать копию своих файлов, `pull --force`, вручную смержить
---
## Работа в команде с Git
Можно хранить эндпоинты в Git-репозитории. Добавьте в `.gitignore`:
```
.kisync.json
.kisync-state.json
```
Workflow:
1. `kisync pull` — скачать с сервера
2. `git commit` — зафиксировать
3. Редактировать файлы
4. `kisync push` — отправить на сервер
5. `git commit` — зафиксировать изменения
---
## Сборка из исходников
```bash
git clone ssh://git@gitea.esh-service.ru:2222/public/api_builder_cli_client.git
cd api_builder_cli_client
npm install
npm run package # → release/kisync.exe
```

View File

@@ -1,55 +1,95 @@
@echo off @echo off
setlocal setlocal EnableDelayedExpansion
set "INSTALL_DIR=%LOCALAPPDATA%\kisync" set "INSTALL_DIR=%LOCALAPPDATA%\kisync"
set "EXE_NAME=kisync.exe" set "EXE_NAME=kisync.exe"
set "DOWNLOAD_URL=https://gitea.esh-service.ru/public/api_builder_cli_client/releases/download/v1.0.0/kisync.exe"
echo. echo.
echo KIS API Builder Sync - Installer echo ==========================================
echo ================================= echo kisync - KIS API Builder Sync - Installer
echo ==========================================
echo. echo.
:: Create install directory :: Create install directory
if not exist "%INSTALL_DIR%" ( if not exist "%INSTALL_DIR%" (
mkdir "%INSTALL_DIR%" mkdir "%INSTALL_DIR%"
echo Created: %INSTALL_DIR% echo [+] Created directory: %INSTALL_DIR%
) else (
echo [=] Directory exists: %INSTALL_DIR%
) )
:: Copy exe :: Check if exe is next to this script (offline install)
copy /Y "%~dp0release\%EXE_NAME%" "%INSTALL_DIR%\%EXE_NAME%" >nul 2>&1 if exist "%~dp0release\%EXE_NAME%" (
if errorlevel 1 ( echo [~] Found local exe, copying...
echo ERROR: Could not copy %EXE_NAME%. Make sure release\kisync.exe exists. copy /Y "%~dp0release\%EXE_NAME%" "%INSTALL_DIR%\%EXE_NAME%" >nul 2>&1
echo Run "npm run package" first to build the exe. goto :check_copy
)
if exist "%~dp0%EXE_NAME%" (
echo [~] Found local exe, copying...
copy /Y "%~dp0%EXE_NAME%" "%INSTALL_DIR%\%EXE_NAME%" >nul 2>&1
goto :check_copy
)
:: Download from Gitea
echo [~] Downloading kisync.exe ...
echo %DOWNLOAD_URL%
echo.
powershell -Command "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; (New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%INSTALL_DIR%\%EXE_NAME%')" 2>nul
:check_copy
if not exist "%INSTALL_DIR%\%EXE_NAME%" (
echo.
echo [!] ERROR: Failed to get kisync.exe
echo Try downloading manually from:
echo %DOWNLOAD_URL%
echo and place it in: %INSTALL_DIR%\
echo.
pause pause
exit /b 1 exit /b 1
) )
echo Installed: %INSTALL_DIR%\%EXE_NAME%
:: Check if already in PATH :: Show version
echo %PATH% | findstr /I /C:"%INSTALL_DIR%" >nul 2>&1 echo [+] Installed: %INSTALL_DIR%\%EXE_NAME%
if %errorlevel%==0 ( "%INSTALL_DIR%\%EXE_NAME%" --version 2>nul && echo.
echo PATH: already configured
goto :done
)
:: Add to user PATH :: Check if already in user PATH
echo Adding to user PATH... set "NEED_PATH=1"
for /f "tokens=2*" %%a in ('reg query "HKCU\Environment" /v Path 2^>nul') do set "USER_PATH=%%b" for /f "tokens=2*" %%a in ('reg query "HKCU\Environment" /v Path 2^>nul') do set "USER_PATH=%%b"
if defined USER_PATH ( if defined USER_PATH (
reg add "HKCU\Environment" /v Path /t REG_EXPAND_SZ /d "%USER_PATH%;%INSTALL_DIR%" /f >nul 2>&1 echo !USER_PATH! | findstr /I /C:"%INSTALL_DIR%" >nul 2>&1
) else ( if !errorlevel!==0 (
reg add "HKCU\Environment" /v Path /t REG_EXPAND_SZ /d "%INSTALL_DIR%" /f >nul 2>&1 set "NEED_PATH=0"
echo [=] PATH: already configured
)
) )
:: Broadcast environment change so new terminals pick it up if !NEED_PATH!==1 (
rundll32.exe user32.dll,UpdatePerIDesktopLayout >nul 2>&1 echo [~] Adding to user PATH...
if defined USER_PATH (
reg add "HKCU\Environment" /v Path /t REG_EXPAND_SZ /d "%USER_PATH%;%INSTALL_DIR%" /f >nul 2>&1
) else (
reg add "HKCU\Environment" /v Path /t REG_EXPAND_SZ /d "%INSTALL_DIR%" /f >nul 2>&1
)
echo PATH: added %INSTALL_DIR% :: Notify system about environment change
powershell -Command "[Environment]::SetEnvironmentVariable('_kisync_refresh','1','User'); [Environment]::SetEnvironmentVariable('_kisync_refresh', $null, 'User')" 2>nul
echo [+] PATH: added %INSTALL_DIR%
)
:done
echo. echo.
echo Done! Open a NEW terminal and run: echo ==========================================
echo kisync --help echo Installation complete!
echo ==========================================
echo.
echo Open a NEW terminal and run:
echo kisync --help
echo.
echo Quick start:
echo cd my-project
echo kisync init
echo kisync pull
echo. echo.
pause pause

View File

@@ -1,6 +1,6 @@
{ {
"name": "kisync", "name": "kisync",
"version": "1.0.0", "version": "1.1.0",
"description": "CLI tool for syncing local folders with KIS API Builder", "description": "CLI tool for syncing local folders with KIS API Builder",
"main": "dist/index.js", "main": "dist/index.js",
"bin": { "bin": {

View File

@@ -1,3 +1,5 @@
import * as fs from 'fs';
import * as path from 'path';
import * as readline from 'readline'; import * as readline from 'readline';
import chalk from 'chalk'; import chalk from 'chalk';
import { writeConfig, findProjectRoot } from '../config'; import { writeConfig, findProjectRoot } from '../config';
@@ -91,11 +93,173 @@ export async function initCommand(): Promise<void> {
writeConfig({ host, token }, cwd); writeConfig({ host, token }, cwd);
// Write default context file for AI assistants
writeDefaultContext(cwd);
console.log(chalk.green('\nProject initialized successfully!')); console.log(chalk.green('\nProject initialized successfully!'));
console.log(chalk.gray(`Config saved to: ${cwd}/.kisync.json`)); console.log(chalk.gray(`Config saved to: ${cwd}/.kisync.json`));
console.log(chalk.gray(`AI context: ${cwd}/CONTEXT.md`));
console.log(''); console.log('');
console.log('Next steps:'); console.log('Next steps:');
console.log(` ${chalk.cyan('kisync pull')} — download endpoints from server`); console.log(` ${chalk.cyan('kisync pull')} — download endpoints from server`);
console.log(` ${chalk.cyan('kisync status')} — check what changed`); console.log(` ${chalk.cyan('kisync status')} — check what changed`);
console.log(` ${chalk.cyan('kisync push')} — upload your changes`); console.log(` ${chalk.cyan('kisync push')} — upload your changes`);
} }
function writeDefaultContext(cwd: string): void {
const contextPath = path.join(cwd, 'CONTEXT.md');
if (fs.existsSync(contextPath)) return; // don't overwrite
const content = `# KIS API Builder — Project Context
This directory contains endpoint definitions synced from a KIS API Builder server via \`kisync\`.
## Directory Structure
\`\`\`
project-root/
├── .kisync.json # connection config (host, token) — DO NOT COMMIT
├── .kisync-state.json # sync state (hashes, timestamps) — DO NOT EDIT
├── CONTEXT.md # this file
├── FolderName/ # API folder
│ ├── _folder.json # folder metadata (id, name, parent_id)
│ └── EndpointName/ # single API endpoint
│ ├── endpoint.json # endpoint metadata (method, path, params, config)
│ ├── query.sql # SQL query (for sql-type endpoints)
│ ├── main.js # JavaScript script (for script-type endpoints)
│ ├── main.py # Python script (for script-type endpoints)
│ ├── request.http # HTTP request definition (for AQL-type endpoints)
│ └── queries/ # named queries used by scripts
│ ├── _index.json # query index (name → file, database binding)
│ ├── get_users.sql # SQL query file
│ └── get_data.http # AQL/HTTP query file
\`\`\`
## Script Execution Model
Scripts in \`main.js\` / \`main.py\` are NOT standalone programs.
They run inside an API Builder sandbox with pre-injected globals.
### IMPORTANT: Do NOT wrap code in functions
WRONG:
\`\`\`js
function main() {
const result = await execQuery('get_users');
return result.data;
}
main();
\`\`\`
CORRECT — write code directly at the top level:
\`\`\`js
const result = await execQuery('get_users');
return result.data;
\`\`\`
The server wraps the code in \`(async function() { <your code> })()\` automatically.
A top-level \`return\` statement sets the API response body.
### Available Globals (JavaScript)
| Global | Description |
|---|---|
| \`params\` | Object with request parameters (query string + body). Example: \`params.user_id\` |
| \`execQuery(name, extraParams?)\` | Execute a named query from the \`queries/\` folder. Returns \`{ success, data, rowCount, error }\` |
| \`console.log/warn/error/info\` | Captured logs, visible in API Builder UI |
| \`JSON, Date, Math, Array, Object, String, Number, Boolean, RegExp, Map, Set, Promise\` | Standard JS built-ins |
| \`setTimeout(fn, ms)\` | Capped at 30 seconds |
| \`parseInt, parseFloat, isNaN, isFinite\` | Standard utility functions |
| \`encodeURIComponent, decodeURIComponent, encodeURI, decodeURI\` | URL encoding |
No \`require\`, \`import\`, \`fetch\`, \`fs\`, \`process\`, or \`Buffer\` — the sandbox is isolated.
### Available Globals (Python)
| Global | Description |
|---|---|
| \`params\` | Dict with request parameters. Example: \`params["user_id"]\` |
| \`exec_query(name, additional_params=None)\` | Execute a named query. Returns dict \`{ "success", "data", "rowCount", "error" }\` |
| \`json, sys, datetime\` | Pre-imported standard modules |
Use \`return\` to set the response. The code is wrapped in a function automatically.
### execQuery / exec_query Explained
The function \`execQuery\` (JS) or \`exec_query\` (Python) runs a query defined in the \`queries/\` folder.
Each query file is mapped via \`queries/_index.json\`:
\`\`\`json
[
{
"name": "get_users",
"database_id": "abc-123",
"file": "get_users.sql"
},
{
"name": "get_data",
"database_id": "def-456",
"file": "get_data.http",
"type": "aql"
}
]
\`\`\`
**The \`name\` field is the key** — it's what you pass to \`execQuery("get_users")\`.
The \`file\` field is the corresponding SQL or HTTP file in the \`queries/\` directory.
SQL queries use \`$paramName\` placeholders that auto-bind from \`params\`:
\`\`\`sql
SELECT * FROM users WHERE id = $user_id AND status = $status
\`\`\`
You can also pass extra params:
\`\`\`js
const result = await execQuery('get_users', { status: 'active' });
// result.data — array of rows
// result.success — boolean
// result.rowCount — number of rows
\`\`\`
### AQL (HTTP) Queries
\`.http\` files define HTTP requests to external APIs:
\`\`\`http
POST https://example.com/api/patients
Content-Type: application/json
{
"name": "{{name}}",
"age": {{age}}
}
\`\`\`
### endpoint.json Fields
| Field | Description |
|---|---|
| \`id\` | Server-side UUID — do not change |
| \`name\` | Display name of the endpoint |
| \`method\` | HTTP method: GET, POST, PUT, DELETE |
| \`path\` | URL path, e.g. \`/api/users\` |
| \`execution_type\` | \`"sql"\`, \`"script"\`, or \`"aql"\` |
| \`parameters\` | Array of \`{ name, type, required, default_value, description }\` |
| \`database_name\` | Bound database name (for sql-type) |
| \`database_id\` | Bound database UUID |
| \`updated_at\` | Last server update timestamp — do not change |
## Sync Workflow
\`\`\`bash
kisync pull # download from server → local files
# ... edit files ...
kisync status # see what changed locally and on server
kisync push # upload changes (with conflict detection)
\`\`\`
Conflict detection: if someone else modified an endpoint on the server since your
last pull, \`push\` will warn you. Use \`--force\` to overwrite, or \`pull\` first to merge.
`;
fs.writeFileSync(contextPath, content, 'utf-8');
}

View File

@@ -252,7 +252,10 @@ export async function pullCommand(force = false): Promise<void> {
writeEndpointToDisk(ep, endpointDir); writeEndpointToDisk(ep, endpointDir);
const hash = computeEndpointHash(ep); // Хеш считаем от того, что реально записалось на диск,
// чтобы он совпадал при последующем readEndpointFromDisk в status
const diskEp = readEndpointFromDisk(endpointDir);
const hash = computeEndpointHash(diskEp || ep);
newState.endpoints[ep.id] = { newState.endpoints[ep.id] = {
updated_at: ep.updated_at, updated_at: ep.updated_at,
folder_path: path.relative(root, endpointDir), folder_path: path.relative(root, endpointDir),

218
src/commands/update.ts Normal file
View 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}\\`));
}
}

View File

@@ -6,13 +6,16 @@ import { initCommand } from './commands/init';
import { pullCommand } from './commands/pull'; import { pullCommand } from './commands/pull';
import { pushCommand } from './commands/push'; import { pushCommand } from './commands/push';
import { statusCommand } from './commands/status'; import { statusCommand } from './commands/status';
import { updateCommand, updateApplyCommand, checkForUpdateBackground } from './commands/update';
const VERSION = '1.1.0';
const program = new Command(); const program = new Command();
program program
.name('kisync') .name('kisync')
.description('CLI tool for syncing local folders with KIS API Builder') .description('CLI tool for syncing local folders with KIS API Builder')
.version('1.0.0'); .version(VERSION);
program program
.command('init') .command('init')
@@ -20,8 +23,9 @@ program
.action(async () => { .action(async () => {
try { try {
await initCommand(); await initCommand();
await checkForUpdateBackground(VERSION);
} catch (err: any) { } catch (err: any) {
console.error(chalk.red(`Error: ${err.message}`)); console.error(chalk.red(`Ошибка: ${err.message}`));
process.exit(1); process.exit(1);
} }
}); });
@@ -33,8 +37,9 @@ program
.action(async (opts) => { .action(async (opts) => {
try { try {
await pullCommand(opts.force); await pullCommand(opts.force);
await checkForUpdateBackground(VERSION);
} catch (err: any) { } catch (err: any) {
console.error(chalk.red(`Error: ${err.message}`)); console.error(chalk.red(`Ошибка: ${err.message}`));
process.exit(1); process.exit(1);
} }
}); });
@@ -46,8 +51,9 @@ program
.action(async (opts) => { .action(async (opts) => {
try { try {
await pushCommand(opts.force); await pushCommand(opts.force);
await checkForUpdateBackground(VERSION);
} catch (err: any) { } catch (err: any) {
console.error(chalk.red(`Error: ${err.message}`)); console.error(chalk.red(`Ошибка: ${err.message}`));
process.exit(1); process.exit(1);
} }
}); });
@@ -58,8 +64,26 @@ program
.action(async () => { .action(async () => {
try { try {
await statusCommand(); await statusCommand();
await checkForUpdateBackground(VERSION);
} catch (err: any) { } 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); process.exit(1);
} }
}); });