@shirokuma-library/mcp-knowledge-base
Version:
MCP server for AI-powered knowledge management with semantic search, graph analysis, and automatic enrichment
232 lines (231 loc) • 10.8 kB
JavaScript
import { Command } from 'commander';
import chalk from 'chalk';
import { AppDataSource } from '../../data-source.js';
import { ExportManager } from '../../services/export-manager.js';
import { Item } from '../../entities/Item.js';
import { Status } from '../../entities/Status.js';
import { ItemTag } from '../../entities/ItemTag.js';
import Table from 'cli-table3';
import path from 'path';
export function createExportCommand() {
const exportCmd = new Command('export')
.description('Export items to files');
const exportManager = new ExportManager();
exportCmd
.argument('[id]', 'Export specific item by ID or "state" for current state')
.option('-t, --type <type>', 'Filter by type')
.option('-s, --status <status...>', 'Filter by status')
.option('--tags <tags...>', 'Filter by tags')
.option('-l, --limit <number>', 'Limit number of items', parseInt)
.option('-d, --dir <directory>', 'Export directory (overrides SHIROKUMA_EXPORT_DIR)')
.option('--include-state', 'Include current system state in export')
.option('--all-states', 'Export all system state history (with --include-state or "state" command)')
.action(async (id, options) => {
try {
if (!AppDataSource.isInitialized) {
await AppDataSource.initialize();
}
if (options.dir) {
const normalizedPath = path.resolve(options.dir);
const safeBasePath = path.resolve(process.cwd());
if (!normalizedPath.startsWith(safeBasePath) && !normalizedPath.startsWith('/tmp')) {
console.error(chalk.red('Error: Export directory must be within project directory or /tmp'));
process.exit(1);
}
process.env.SHIROKUMA_EXPORT_DIR = normalizedPath;
}
let result;
if (id === 'state') {
const exportAll = options.allStates || false;
console.log(chalk.cyan(exportAll ? 'Exporting all system states...' : 'Exporting current system state...'));
const stateResult = await exportManager.exportCurrentState(exportAll);
if (stateResult.exported) {
if (stateResult.count && stateResult.count > 1) {
console.log(chalk.green(`\n✓ Exported ${stateResult.count} system states to ${stateResult.directory}/.system/current_state/`));
}
else {
console.log(chalk.green(`\n✓ Exported system state to ${stateResult.directory}/${stateResult.file}`));
}
console.log(chalk.gray(` Latest symlink: ${stateResult.directory}/.system/current_state/latest.md`));
}
else {
console.log(chalk.yellow('\n⚠ No system state found to export'));
}
await AppDataSource.destroy();
return;
}
else if (id) {
const itemId = parseInt(id);
if (isNaN(itemId)) {
console.error(chalk.red('Error: Invalid item ID'));
process.exit(1);
}
console.log(chalk.cyan(`Exporting item ${itemId}...`));
result = await exportManager.exportItem(itemId);
}
else {
console.log(chalk.cyan('Exporting items...'));
result = await exportManager.exportItems({
type: options.type,
status: options.status,
tags: options.tags,
limit: options.limit,
includeState: options.includeState,
includeAllStates: options.allStates
});
}
console.log(chalk.green(`\n✓ Exported ${result.exported} item(s) to ${result.directory}`));
if (result.files.length > 0) {
console.log(chalk.cyan('\nExported files:'));
const filesByType = new Map();
for (const file of result.files) {
const type = file.split('/')[0];
if (!filesByType.has(type)) {
filesByType.set(type, []);
}
filesByType.get(type).push(file);
}
for (const [type, files] of filesByType) {
console.log(chalk.yellow(`\n ${type}/`));
for (const file of files) {
const filename = file.split('/')[1];
console.log(` ${filename}`);
}
}
}
await AppDataSource.destroy();
}
catch (error) {
console.error(chalk.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
await AppDataSource.destroy();
process.exit(1);
}
});
exportCmd
.command('preview')
.description('Preview items that would be exported')
.option('-t, --type <type>', 'Filter by type')
.option('-s, --status <status...>', 'Filter by status')
.option('--tags <tags...>', 'Filter by tags')
.option('-l, --limit <number>', 'Limit number of items', parseInt)
.action(async (options) => {
try {
if (!AppDataSource.isInitialized) {
await AppDataSource.initialize();
}
const itemRepo = AppDataSource.getRepository(Item);
const statusRepo = AppDataSource.getRepository(Status);
const itemTagRepo = AppDataSource.getRepository(ItemTag);
const query = itemRepo.createQueryBuilder('item')
.leftJoinAndSelect('item.status', 'status');
if (options.type) {
query.andWhere('item.type = :type', { type: options.type });
}
if (options.status && options.status.length > 0) {
query.andWhere('status.name IN (:...statuses)', { statuses: options.status });
}
if (options.tags && options.tags.length > 0) {
query.innerJoin('item_tags', 'it', 'it.item_id = item.id')
.innerJoin('tags', 't', 't.id = it.tag_id')
.andWhere('t.name IN (:...tags)', { tags: options.tags });
}
if (options.limit) {
query.limit(options.limit);
}
else {
query.limit(100);
}
query.orderBy('item.updatedAt', 'DESC');
const items = await query.getMany();
if (items.length === 0) {
console.log(chalk.yellow('No items found matching the criteria'));
await AppDataSource.destroy();
return;
}
const table = new Table({
head: ['ID', 'Type', 'Title', 'Status', 'Tags'],
colWidths: [8, 15, 40, 15, 30],
wordWrap: true
});
for (const item of items) {
const itemTags = await itemTagRepo.find({
where: { itemId: item.id },
relations: ['tag']
});
const tags = itemTags.map(it => it.tag.name).join(', ');
const status = await statusRepo.findOne({ where: { id: item.statusId } });
table.push([
item.id,
item.type,
item.title.substring(0, 37) + (item.title.length > 37 ? '...' : ''),
status?.name || 'Unknown',
tags || '-'
]);
}
console.log(chalk.cyan(`\nFound ${items.length} item(s) to export:\n`));
console.log(table.toString());
const exportDir = process.env.SHIROKUMA_EXPORT_DIR || 'docs/export';
console.log(chalk.gray(`\nWould export to: ${exportDir}`));
await AppDataSource.destroy();
}
catch (error) {
console.error(chalk.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
await AppDataSource.destroy();
process.exit(1);
}
});
exportCmd
.command('all')
.description('Export all items including system state')
.option('-d, --dir <directory>', 'Export directory (overrides SHIROKUMA_EXPORT_DIR)')
.action(async (options) => {
try {
if (!AppDataSource.isInitialized) {
await AppDataSource.initialize();
}
if (options.dir) {
const normalizedPath = path.resolve(options.dir);
const safeBasePath = path.resolve(process.cwd());
if (!normalizedPath.startsWith(safeBasePath) && !normalizedPath.startsWith('/tmp')) {
console.error(chalk.red('Error: Export directory must be within project directory or /tmp'));
process.exit(1);
}
process.env.SHIROKUMA_EXPORT_DIR = normalizedPath;
}
console.log(chalk.cyan('Exporting all items and system state...'));
const result = await exportManager.exportItems({
includeState: true,
includeAllStates: true
});
console.log(chalk.green(`\n✓ Exported ${result.exported} item(s) to ${result.directory}`));
if (result.stateExported) {
console.log(chalk.green('✓ Exported system state to .system/current_state/'));
}
if (result.files.length > 0) {
console.log(chalk.cyan('\nExported files:'));
const filesByType = new Map();
for (const file of result.files) {
const type = file.split('/')[0];
if (!filesByType.has(type)) {
filesByType.set(type, []);
}
filesByType.get(type).push(file);
}
for (const [type, files] of filesByType) {
console.log(chalk.yellow(`\n ${type}/`));
for (const file of files) {
const filename = file.split('/').slice(1).join('/');
console.log(` ${filename}`);
}
}
}
await AppDataSource.destroy();
}
catch (error) {
console.error(chalk.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
await AppDataSource.destroy();
process.exit(1);
}
});
return exportCmd;
}