Initial commit: kisync CLI client for KIS API Builder

CLI tool for syncing local folders with KIS API Builder server.
Commands: init, pull, push, status with conflict detection.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-14 16:19:48 +03:00
commit 4c54bfff26
13 changed files with 1882 additions and 0 deletions

119
src/commands/status.ts Normal file
View File

@@ -0,0 +1,119 @@
import * as path from 'path';
import chalk from 'chalk';
import { readConfig, readState, getProjectRoot } from '../config';
import { ApiClient } from '../api';
import { findEndpointDirs, readEndpointFromDisk } from '../files';
import { computeEndpointHash } from '../hash';
export async function statusCommand(): Promise<void> {
const root = getProjectRoot();
const config = readConfig(root);
const state = readState(root);
const api = new ApiClient(config);
if (!state.last_sync) {
console.log(chalk.yellow('No sync history. Run "kisync pull" first.'));
return;
}
console.log(chalk.gray(`Last sync: ${state.last_sync}\n`));
// 1) Detect local changes
const localModified: string[] = [];
const localNew: string[] = [];
const endpointDirs = findEndpointDirs(root);
const knownIds = new Set(Object.keys(state.endpoints));
const foundIds = new Set<string>();
for (const dir of endpointDirs) {
const ep = readEndpointFromDisk(dir);
if (!ep) continue;
if (ep.id && knownIds.has(ep.id)) {
foundIds.add(ep.id);
const stateEntry = state.endpoints[ep.id];
if (stateEntry && stateEntry.hash) {
const currentHash = computeEndpointHash(ep);
if (currentHash !== stateEntry.hash) {
localModified.push(`${path.relative(root, dir)} (${ep.name})`);
}
}
} else if (!ep.id) {
// New local endpoint (no id assigned yet)
localNew.push(`${path.relative(root, dir)} (${ep.name || 'unnamed'})`);
}
}
// Locally deleted (existed in state but no longer on disk)
const localDeleted: string[] = [];
for (const [id, info] of Object.entries(state.endpoints)) {
if (!foundIds.has(id)) {
localDeleted.push(`${info.folder_path}`);
}
}
// 2) Check server changes
const clientEndpoints = Object.entries(state.endpoints).map(([id, info]) => ({
id,
updated_at: info.updated_at,
}));
const clientFolders = Object.entries(state.folders).map(([id, info]) => ({
id,
updated_at: info.updated_at,
}));
console.log(chalk.gray('Checking server...'));
const serverStatus = await api.status({ endpoints: clientEndpoints, folders: clientFolders });
// 3) Display results
const hasLocalChanges = localModified.length > 0 || localNew.length > 0 || localDeleted.length > 0;
const hasServerChanges =
serverStatus.endpoints.changed.length > 0 ||
serverStatus.endpoints.new.length > 0 ||
serverStatus.endpoints.deleted.length > 0;
if (!hasLocalChanges && !hasServerChanges) {
console.log(chalk.green('\nEverything is in sync.'));
return;
}
// Local changes
if (hasLocalChanges) {
console.log(chalk.bold('\nLocal changes (not pushed):'));
for (const item of localModified) {
console.log(chalk.yellow(` modified: ${item}`));
}
for (const item of localNew) {
console.log(chalk.green(` new: ${item}`));
}
for (const item of localDeleted) {
console.log(chalk.red(` deleted: ${item}`));
}
}
// Server changes
if (hasServerChanges) {
console.log(chalk.bold('\nServer changes (not pulled):'));
for (const item of serverStatus.endpoints.changed) {
console.log(chalk.blue(` modified: ${item.name}`));
}
for (const item of serverStatus.endpoints.new) {
console.log(chalk.green(` new: ${item.name}`));
}
for (const item of serverStatus.endpoints.deleted) {
console.log(chalk.red(` deleted: id=${item.id}`));
}
}
// Conflicts warning
if (hasLocalChanges && hasServerChanges) {
console.log(chalk.yellow('\nBoth local and server have changes!'));
console.log(chalk.gray(' Push first to send your changes, then pull to get server updates.'));
console.log(chalk.gray(' Or use --force on either to overwrite.'));
} else if (hasLocalChanges) {
console.log(chalk.gray('\nRun "kisync push" to upload your changes.'));
} else {
console.log(chalk.gray('\nRun "kisync pull" to download server changes.'));
}
}