claude-buddy
Version:
Claude Buddy installation and setup script
198 lines (167 loc) • 6.65 kB
JavaScript
/**
* Bundle Distribution Script
*
* Copies source files from repository root to setup/dist/ for npm distribution.
* This ensures the npm package contains all necessary source files for installation.
*/
const fs = require('fs-extra');
const path = require('path');
const ROOT_DIR = path.resolve(__dirname, '..', '..');
const SETUP_DIR = path.resolve(__dirname, '..');
const DIST_DIR = path.join(SETUP_DIR, 'dist');
// Files and directories to exclude from bundling
const EXCLUDE_PATTERNS = [
'settings.local.json', // User-specific settings
'install-metadata.json', // Installation-specific metadata
'.DS_Store', // macOS system files
'Thumbs.db' // Windows system files
];
/**
* Check if a file should be excluded from bundling
*/
function shouldExclude(filePath) {
const basename = path.basename(filePath);
return EXCLUDE_PATTERNS.some(pattern => basename === pattern);
}
/**
* Copy directory recursively with exclusions
*/
async function copyDirWithExclusions(src, dest) {
await fs.ensureDir(dest);
const entries = await fs.readdir(src, { withFileTypes: true });
let copiedCount = 0;
for (const entry of entries) {
const srcPath = path.join(src, entry.name);
const destPath = path.join(dest, entry.name);
if (shouldExclude(srcPath)) {
console.log(` ⊗ Skipping: ${path.relative(ROOT_DIR, srcPath)}`);
continue;
}
if (entry.isDirectory()) {
const count = await copyDirWithExclusions(srcPath, destPath);
copiedCount += count;
} else {
await fs.copy(srcPath, destPath);
copiedCount++;
console.log(` ✓ Copied: ${path.relative(ROOT_DIR, srcPath)}`);
}
}
return copiedCount;
}
/**
* Transform absolute paths to relative paths in hooks.json
*/
async function transformHooksJson(hooksPath) {
const content = await fs.readFile(hooksPath, 'utf8');
const hooks = JSON.parse(content);
// Regex to match absolute paths to .claude/hooks/ while preserving the command prefix
// Matches: command prefix + space + absolute path starting with / + .claude/hooks/ + filename
// Captures: everything before the path starting with / (group 1) and the filename (group 2)
const absolutePathRegex = /^(.*\s)\/[^/]+.*\/\.claude\/hooks\/(.+)$/;
let transformedCount = 0;
// Transform paths in PreToolUse hooks
if (hooks.hooks?.PreToolUse) {
for (const matcher of hooks.hooks.PreToolUse) {
for (const hook of matcher.hooks || []) {
if (hook.command && absolutePathRegex.test(hook.command)) {
hook.command = hook.command.replace(absolutePathRegex, '$1.claude/hooks/$2');
transformedCount++;
}
}
}
}
// Transform paths in PostToolUse hooks
if (hooks.hooks?.PostToolUse) {
for (const matcher of hooks.hooks.PostToolUse) {
for (const hook of matcher.hooks || []) {
if (hook.command && absolutePathRegex.test(hook.command)) {
hook.command = hook.command.replace(absolutePathRegex, '$1.claude/hooks/$2');
transformedCount++;
}
}
}
}
if (transformedCount > 0) {
await fs.writeFile(hooksPath, JSON.stringify(hooks, null, 2) + '\n', 'utf8');
console.log(` ✓ Transformed ${transformedCount} absolute paths to relative`);
}
return transformedCount;
}
/**
* Main bundling process
*/
async function bundle() {
console.log('═══════════════════════════════════════════════════');
console.log(' Claude Buddy Distribution Bundle');
console.log('═══════════════════════════════════════════════════\n');
try {
// Clean and create dist directory
console.log('📦 Preparing distribution directory...');
await fs.remove(DIST_DIR);
await fs.ensureDir(DIST_DIR);
console.log(` ✓ Created: ${path.relative(SETUP_DIR, DIST_DIR)}\n`);
// Bundle .claude directory
console.log('📁 Bundling .claude directory...');
const claudeSrc = path.join(ROOT_DIR, '.claude');
const claudeDest = path.join(DIST_DIR, '.claude');
if (await fs.pathExists(claudeSrc)) {
const claudeCount = await copyDirWithExclusions(claudeSrc, claudeDest);
console.log(` ✓ Bundled ${claudeCount} files from .claude/\n`);
// Transform hooks.json paths to relative
console.log('🔄 Transforming paths in hooks.json...');
const hooksJsonPath = path.join(claudeDest, 'hooks.json');
if (await fs.pathExists(hooksJsonPath)) {
await transformHooksJson(hooksJsonPath);
} else {
console.log(' ⊗ hooks.json not found, skipping transformation');
}
console.log('');
} else {
console.error(` ✗ Source not found: ${claudeSrc}`);
process.exit(1);
}
// Verify bundle contents
console.log('🔍 Verifying bundle...');
const verifyPaths = [
path.join(claudeDest, 'agents'),
path.join(claudeDest, 'commands'),
path.join(claudeDest, 'hooks'),
path.join(claudeDest, 'hooks.json'),
path.join(claudeDest, 'skills'),
path.join(claudeDest, 'skills', 'personas'),
path.join(claudeDest, 'skills', 'domains'),
path.join(claudeDest, 'skills', 'generators')
];
let allValid = true;
for (const verifyPath of verifyPaths) {
const exists = await fs.pathExists(verifyPath);
const relativePath = path.relative(DIST_DIR, verifyPath);
if (exists) {
console.log(` ✓ Found: ${relativePath}`);
} else {
console.error(` ✗ Missing: ${relativePath}`);
allValid = false;
}
}
if (!allValid) {
console.error('\n✗ Bundle verification failed!');
process.exit(1);
}
console.log('\n═══════════════════════════════════════════════════');
console.log(' ✓ Distribution bundle created successfully');
console.log('═══════════════════════════════════════════════════\n');
} catch (error) {
console.error('\n✗ Bundle creation failed:', error.message);
console.error(error.stack);
process.exit(1);
}
}
// Run bundle if executed directly
if (require.main === module) {
bundle().catch(error => {
console.error('Fatal error:', error);
process.exit(1);
});
}
module.exports = { bundle };