Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5d6e7bbe56 | |||
| 0ad43a6981 | |||
| 9f041c2d3d | |||
| 302c7ab9f5 |
161
README.md
Normal file
161
README.md
Normal 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
|
||||||
|
```
|
||||||
92
install.bat
92
install.bat
@@ -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 Installation complete!
|
||||||
|
echo ==========================================
|
||||||
|
echo.
|
||||||
|
echo Open a NEW terminal and run:
|
||||||
echo kisync --help
|
echo kisync --help
|
||||||
echo.
|
echo.
|
||||||
|
echo Quick start:
|
||||||
|
echo cd my-project
|
||||||
|
echo kisync init
|
||||||
|
echo kisync pull
|
||||||
|
echo.
|
||||||
pause
|
pause
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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');
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
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 { 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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user