bundle-api.jsโข7.78 kB
#!/usr/bin/env node
/**
* Bundle API Script for Claude-Slack
*
* This script copies the API package into the template directory
* so it gets included when users run `npx claude-slack`.
*
* Run with: npm run bundle-api
*/
const fs = require('fs-extra');
const path = require('path');
const { exec } = require('child_process');
const util = require('util');
const execAsync = util.promisify(exec);
// Colors for console output
const colors = {
reset: '\x1b[0m',
bright: '\x1b[1m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
red: '\x1b[31m',
cyan: '\x1b[36m'
};
function log(message, color = 'reset') {
console.log(`${colors[color]}${message}${colors.reset}`);
}
async function bundleAPI() {
log('\n๐ฆ Claude-Slack API Bundling Script', 'bright');
log('='.repeat(50), 'blue');
// Define paths
const rootDir = path.join(__dirname, '..');
const apiSource = path.join(rootDir, 'api');
const templateDest = path.join(rootDir, 'template', 'global', 'mcp', 'claude-slack', 'api');
// Step 1: Verify API source exists
log('\n1๏ธโฃ Checking API source directory...', 'cyan');
if (!fs.existsSync(apiSource)) {
log(`โ API source not found at: ${apiSource}`, 'red');
process.exit(1);
}
log(` โ Found API at: ${apiSource}`, 'green');
// Step 2: Clean destination
log('\n2๏ธโฃ Cleaning destination directory...', 'cyan');
if (fs.existsSync(templateDest)) {
await fs.remove(templateDest);
log(' โ Removed old API bundle', 'green');
}
// Step 3: Copy API with filtering
log('\n3๏ธโฃ Bundling API package...', 'cyan');
let fileCount = 0;
let skippedCount = 0;
const skippedPatterns = [];
await fs.copy(apiSource, templateDest, {
filter: (src, dest) => {
const relativePath = path.relative(apiSource, src);
// Always include directories (to traverse into them)
if (fs.statSync(src).isDirectory()) {
// But skip certain directories entirely
if (src.includes('__pycache__') ||
src.includes('.pytest_cache') ||
src.includes('.git') ||
src.includes('node_modules')) {
skippedPatterns.push(relativePath || path.basename(src));
return false;
}
return true;
}
// Skip unwanted files
if (src.includes('.pyc') || // Compiled Python
src.includes('.pyo') || // Optimized Python
src.includes('.egg-info') || // Package info
src.includes('.DS_Store') || // macOS files
src.endsWith('~') || // Backup files
path.basename(src).startsWith('.')) { // Hidden files
skippedCount++;
return false;
}
// Skip test files if not needed in production
if (src.includes('/tests/') ||
src.includes('/test_') ||
src.endsWith('_test.py')) {
skippedCount++;
return false;
}
// Include everything else
fileCount++;
return true;
}
});
log(` โ Copied ${fileCount} files`, 'green');
if (skippedCount > 0) {
log(` โ Skipped ${skippedCount} unnecessary files`, 'yellow');
}
if (skippedPatterns.length > 0) {
log(` โ Excluded patterns: ${[...new Set(skippedPatterns)].join(', ')}`, 'yellow');
}
// Step 4: Create __init__.py if missing
log('\n4๏ธโฃ Ensuring package structure...', 'cyan');
const initPath = path.join(templateDest, '__init__.py');
if (!fs.existsSync(initPath)) {
await fs.writeFile(initPath, '"""Claude-Slack API Package"""\n');
log(' โ Created __init__.py', 'green');
}
// Step 5: Generate requirements.txt for the bundled API
log('\n5๏ธโฃ Generating requirements.txt...', 'cyan');
const requirementsSource = path.join(apiSource, 'requirements.txt');
const requirementsDest = path.join(rootDir, 'template', 'global', 'mcp', 'claude-slack', 'requirements.txt');
// Start with base requirements
let requirements = `# Claude-Slack API Requirements
# Generated by bundle-api.js - DO NOT EDIT MANUALLY
# Core API dependencies
aiosqlite>=0.19.0
qdrant-client>=1.7.0
sentence-transformers>=2.2.0
numpy>=1.24.0
# MCP Server dependencies
mcp>=0.1.0
python-dotenv>=1.0.0
# Optional dependencies
# Uncomment based on your deployment:
# psycopg2-binary>=2.9.0 # For PostgreSQL
# redis>=4.5.0 # For Redis caching
`;
// If there's a source requirements.txt, merge it
if (fs.existsSync(requirementsSource)) {
const sourceReqs = await fs.readFile(requirementsSource, 'utf8');
const customReqs = sourceReqs
.split('\n')
.filter(line => line && !line.startsWith('#') && !requirements.includes(line))
.join('\n');
if (customReqs) {
requirements += '\n# Additional requirements from API\n' + customReqs + '\n';
}
}
await fs.writeFile(requirementsDest, requirements);
log(' โ Generated requirements.txt', 'green');
// Step 8: Verify the bundle
log('\n8๏ธโฃ Verifying bundle integrity...', 'cyan');
const criticalFiles = [
'api/__init__.py',
'api/unified_api.py',
'api/db/sqlite_store.py',
'api/db/qdrant_store.py',
'api/db/message_store.py',
'api/models.py',
'requirements.txt',
];
let allGood = true;
for (const file of criticalFiles) {
const fullPath = path.join(rootDir, 'template', 'global', 'mcp', 'claude-slack', file);
if (fs.existsSync(fullPath)) {
log(` โ ${file}`, 'green');
} else {
log(` โ Missing: ${file}`, 'red');
allGood = false;
}
}
// Step 9: Generate bundle info
log('\n9๏ธโฃ Generating bundle info...', 'cyan');
const bundleInfo = {
bundled_at: new Date().toISOString(),
api_version: '4.1.0',
files_bundled: fileCount,
files_skipped: skippedCount,
bundle_size: await calculateDirSize(templateDest),
requirements: requirements.split('\n').filter(l => l && !l.startsWith('#')).length
};
const bundleInfoPath = path.join(templateDest, 'bundle_info.json');
await fs.writeJson(bundleInfoPath, bundleInfo, { spaces: 2 });
log(' โ Created bundle_info.json', 'green');
// Final summary
log('\n' + '='.repeat(50), 'blue');
if (allGood) {
log('โ
API bundling completed successfully!', 'bright');
log(`\n๐ Bundle Summary:`, 'cyan');
log(` โข Files bundled: ${fileCount}`, 'green');
log(` โข Files skipped: ${skippedCount}`, 'yellow');
log(` โข Bundle size: ${formatBytes(bundleInfo.bundle_size)}`, 'green');
log(` โข Requirements: ${bundleInfo.requirements} packages`, 'green');
log(`\n๐ Next steps:`, 'cyan');
log(` 1. Test locally: npm run test-install`, 'blue');
log(` 2. Publish: npm publish`, 'blue');
log(` 3. Users install: npx claude-slack`, 'blue');
} else {
log('โ ๏ธ Bundle completed with warnings - review missing files', 'yellow');
}
}
async function calculateDirSize(dir) {
let size = 0;
async function walkDir(currentPath) {
const files = await fs.readdir(currentPath);
for (const file of files) {
const filePath = path.join(currentPath, file);
const stats = await fs.stat(filePath);
if (stats.isDirectory()) {
await walkDir(filePath);
} else {
size += stats.size;
}
}
}
await walkDir(dir);
return size;
}
function formatBytes(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
// Run the bundling
bundleAPI().catch(error => {
log(`\nโ Bundling failed: ${error.message}`, 'red');
console.error(error);
process.exit(1);
});