knowhub
Version:
Synchronize AI coding–agent knowledge files (rules, templates, guidelines) across your project.
146 lines • 5.57 kB
JavaScript
import { resolve } from "node:path";
import { loadConfig } from "../config.js";
import { formatError } from "../errors.js";
import { copyDirectory, copyFile, symlinkOrCopy, writeTextFile, } from "../file.js";
import { ConsoleLogger } from "../logger.js";
import { fetchResources } from "../resource.js";
export async function runSync(options = {}) {
const { configPath, dryRun = false, quiet = false, logger } = options;
const actualLogger = logger || new ConsoleLogger(quiet);
const projectRoot = process.cwd();
let created = 0;
let updated = 0;
let skipped = 0;
const errors = [];
try {
const resources = await loadConfig(configPath, projectRoot);
const fetchedResources = await fetchResources(resources, projectRoot, actualLogger);
for (const fetchedResource of fetchedResources) {
const result = await processFetchedResource(fetchedResource, projectRoot, dryRun, actualLogger);
created += result.created;
updated += result.updated;
skipped += result.skipped;
errors.push(...result.errors);
}
printSummary({ created, updated, skipped, errors }, dryRun, actualLogger);
return { created, updated, skipped, errors };
}
catch (error) {
const errorMessage = formatError(error);
actualLogger.error(`Error: ${errorMessage}`);
errors.push(errorMessage);
return { created, updated, skipped, errors };
}
}
async function processFetchedResource(fetchedResource, projectRoot, dryRun, logger) {
const { definition } = fetchedResource;
let created = 0;
let updated = 0;
let skipped = 0;
const errors = [];
for (const outputPath of definition.outputs) {
try {
const absoluteOutputPath = resolve(projectRoot, outputPath);
if (dryRun) {
logger.info(`[DRY RUN] Would process: ${outputPath}`);
continue;
}
const result = await placeResourceAtOutput(fetchedResource, absoluteOutputPath);
created += result.created;
updated += result.updated;
if (result.created > 0) {
logger.success(`✓ Created: ${outputPath}`);
}
else if (result.updated > 0) {
logger.success(`✓ Updated: ${outputPath}`);
}
else {
skipped++;
if (!definition.overwrite) {
logger.skip(`- Skipped: ${outputPath} (already exists, overwrite: false)`);
}
else {
logger.skip(`- Skipped: ${outputPath} (content identical)`);
}
}
}
catch (error) {
const errorMessage = formatError(error);
const fullError = `Failed to process output "${outputPath}": ${errorMessage}`;
errors.push(fullError);
logger.error(`✗ ${fullError}`);
}
}
return { created, updated, skipped, errors };
}
async function placeResourceAtOutput(fetchedResource, outputPath) {
const { definition, localPath, isDirectory, content, pluginMetadata } = fetchedResource;
const { overwrite } = definition;
if (content !== undefined) {
const result = await writeTextFile(outputPath, content, overwrite);
return { created: result.created ? 1 : 0, updated: result.updated ? 1 : 0 };
}
if (localPath === undefined) {
throw new Error("Local path is undefined for local resource");
}
const symlink = definition.plugin === "local" && pluginMetadata?.symlink === true;
if (isDirectory) {
if (symlink) {
const result = await symlinkOrCopy(localPath, outputPath, overwrite, true);
return {
created: result.created ? 1 : 0,
updated: result.updated ? 1 : 0,
};
}
const result = await copyDirectory(localPath, outputPath, overwrite);
return { created: result.created, updated: 0 };
}
if (symlink) {
const result = await symlinkOrCopy(localPath, outputPath, overwrite, false);
return {
created: result.created ? 1 : 0,
updated: result.updated ? 1 : 0,
};
}
const result = await copyFile(localPath, outputPath, overwrite);
return {
created: result.created ? 1 : 0,
updated: result.updated ? 1 : 0,
};
}
function printSummary(result, dryRun, logger) {
const { created, updated, skipped, errors } = result;
const total = created + updated + skipped;
logger.info(`\n${"=".repeat(50)}`);
if (dryRun) {
logger.info("DRY RUN SUMMARY");
}
else {
logger.info("SYNC SUMMARY");
}
logger.info("=".repeat(50));
logger.info(`Total outputs processed: ${total}`);
logger.info(`Created: ${created}`);
logger.info(`Updated: ${updated}`);
logger.info(`Skipped: ${skipped}`);
if (errors.length > 0) {
logger.info(`Errors: ${errors.length}`);
logger.info("\nErrors encountered:");
for (const error of errors) {
logger.info(` • ${error}`);
}
}
logger.info("=".repeat(50));
if (errors.length === 0) {
if (dryRun) {
logger.success("✓ Dry run completed successfully");
}
else {
logger.success("✓ Sync completed successfully");
}
}
else {
logger.warn("⚠ Sync completed with errors");
}
}
//# sourceMappingURL=sync.js.map