Features: - 30+ Custom Skills (cognitive, development, UI/UX, autonomous agents) - RalphLoop autonomous agent integration - Multi-AI consultation (Qwen) - Agent management system with sync capabilities - Custom hooks for session management - MCP servers integration - Plugin marketplace setup - Comprehensive installation script Components: - Skills: always-use-superpowers, ralph, brainstorming, ui-ux-pro-max, etc. - Agents: 100+ agents across engineering, marketing, product, etc. - Hooks: session-start-superpowers, qwen-consult, ralph-auto-trigger - Commands: /brainstorm, /write-plan, /execute-plan - MCP Servers: zai-mcp-server, web-search-prime, web-reader, zread - Binaries: ralphloop wrapper Installation: ./supercharge.sh
580 lines
17 KiB
TypeScript
580 lines
17 KiB
TypeScript
/**
|
|
* Claude Code Plugin Manager - Conduit-style Plugin System
|
|
*
|
|
* Features:
|
|
* - GitHub-based plugin discovery
|
|
* - Secure plugin installation with validation
|
|
* - Version management and updates
|
|
* - Dependency resolution
|
|
* - Plugin lifecycle management
|
|
*/
|
|
|
|
import fs from 'fs/promises'
|
|
import path from 'path'
|
|
import { spawn } from 'child_process'
|
|
import { createHash } from 'crypto'
|
|
|
|
// ============================================================================
|
|
// TYPES AND INTERFACES
|
|
// ============================================================================
|
|
|
|
export interface PluginMetadata {
|
|
name: string
|
|
version: string
|
|
description: string
|
|
author: string
|
|
license?: string
|
|
repository?: string
|
|
homepage?: string
|
|
keywords?: string[]
|
|
claude: {
|
|
minVersion?: string
|
|
maxVersion?: string
|
|
permissions: string[]
|
|
hooks?: HookDefinition[]
|
|
commands?: CommandDefinition[]
|
|
skills?: SkillDefinition[]
|
|
}
|
|
dependencies?: Record<string, string>
|
|
}
|
|
|
|
export interface HookDefinition {
|
|
event: string
|
|
handler: string
|
|
priority?: number
|
|
condition?: string
|
|
}
|
|
|
|
export interface CommandDefinition {
|
|
name: string
|
|
description: string
|
|
handler: string
|
|
permissions?: string[]
|
|
}
|
|
|
|
export interface SkillDefinition {
|
|
name: string
|
|
description: string
|
|
file: string
|
|
}
|
|
|
|
export interface InstalledPlugin {
|
|
metadata: PluginMetadata
|
|
installPath: string
|
|
version: string
|
|
installedAt: string
|
|
lastUpdated: string
|
|
scope: 'global' | 'project'
|
|
projectPath?: string
|
|
enabled: boolean
|
|
integrity: string
|
|
}
|
|
|
|
export interface MarketplaceSource {
|
|
type: 'github' | 'directory' | 'npm'
|
|
url?: string
|
|
repo?: string
|
|
path?: string
|
|
lastUpdated?: string
|
|
}
|
|
|
|
export interface PluginDiscoveryResult {
|
|
name: string
|
|
description: string
|
|
version: string
|
|
author: string
|
|
source: string
|
|
downloads?: number
|
|
stars?: number
|
|
updated: string
|
|
}
|
|
|
|
// ============================================================================
|
|
// PLUGIN MANAGER CLASS
|
|
// ============================================================================
|
|
|
|
export class PluginManager {
|
|
private pluginsDir: string
|
|
private cacheDir: string
|
|
private marketplacesFile: string
|
|
private installedFile: string
|
|
private configDir: string
|
|
|
|
constructor(claudeDir: string = path.join(process.env.HOME || '', '.claude')) {
|
|
this.configDir = claudeDir
|
|
this.pluginsDir = path.join(claudeDir, 'plugins')
|
|
this.cacheDir = path.join(this.pluginsDir, 'cache')
|
|
this.marketplacesFile = path.join(this.pluginsDir, 'known_marketplaces.json')
|
|
this.installedFile = path.join(this.pluginsDir, 'installed_plugins.json')
|
|
}
|
|
|
|
// ============================================================================
|
|
// INITIALIZATION
|
|
// ============================================================================
|
|
|
|
async initialize(): Promise<void> {
|
|
await this.ensureDirectories()
|
|
await this.loadMarketplaces()
|
|
await this.loadInstalledPlugins()
|
|
}
|
|
|
|
private async ensureDirectories(): Promise<void> {
|
|
const dirs = [
|
|
this.pluginsDir,
|
|
this.cacheDir,
|
|
path.join(this.pluginsDir, 'marketplaces'),
|
|
path.join(this.pluginsDir, 'tmp'),
|
|
]
|
|
|
|
for (const dir of dirs) {
|
|
try {
|
|
await fs.mkdir(dir, { recursive: true })
|
|
} catch (error) {
|
|
// Directory might already exist
|
|
}
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// MARKETPLACE MANAGEMENT
|
|
// ============================================================================
|
|
|
|
async addMarketplace(name: string, source: MarketplaceSource): Promise<void> {
|
|
const marketplaces = await this.loadMarketplaces()
|
|
|
|
marketplaces[name] = {
|
|
source,
|
|
installLocation: path.join(this.pluginsDir, 'marketplaces', name),
|
|
lastUpdated: new Date().toISOString(),
|
|
}
|
|
|
|
await fs.writeFile(
|
|
this.marketplacesFile,
|
|
JSON.stringify(marketplaces, null, 2)
|
|
)
|
|
|
|
// Clone/download marketplace if it's a GitHub repo
|
|
if (source.type === 'github' && source.repo) {
|
|
await this.cloneRepository(
|
|
`https://github.com/${source.repo}.git`,
|
|
path.join(this.pluginsDir, 'marketplaces', name)
|
|
)
|
|
}
|
|
}
|
|
|
|
async loadMarketplaces(): Promise<Record<string, any>> {
|
|
try {
|
|
const content = await fs.readFile(this.marketplacesFile, 'utf-8')
|
|
return JSON.parse(content)
|
|
} catch {
|
|
return {}
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// PLUGIN DISCOVERY
|
|
// ============================================================================
|
|
|
|
async discoverPlugins(query?: string): Promise<PluginDiscoveryResult[]> {
|
|
const marketplaces = await this.loadMarketplaces()
|
|
const results: PluginDiscoveryResult[] = []
|
|
|
|
for (const [name, marketplace]: Object.entries(marketplaces)) {
|
|
const mp = marketplace as any
|
|
const pluginsPath = path.join(mp.installLocation, 'plugins')
|
|
|
|
try {
|
|
const pluginDirs = await fs.readdir(pluginsPath)
|
|
|
|
for (const pluginDir of pluginDirs) {
|
|
const pluginJsonPath = path.join(
|
|
pluginsPath,
|
|
pluginDir,
|
|
'.claude-plugin',
|
|
'plugin.json'
|
|
)
|
|
|
|
try {
|
|
const metadata = JSON.parse(
|
|
await fs.readFile(pluginJsonPath, 'utf-8')
|
|
) as PluginMetadata
|
|
|
|
// Filter by query if provided
|
|
if (
|
|
query &&
|
|
!metadata.name.toLowerCase().includes(query.toLowerCase()) &&
|
|
!metadata.description?.toLowerCase().includes(query.toLowerCase()) &&
|
|
!metadata.keywords?.some((k) =>
|
|
k.toLowerCase().includes(query.toLowerCase())
|
|
)
|
|
) {
|
|
continue
|
|
}
|
|
|
|
results.push({
|
|
name: metadata.name,
|
|
description: metadata.description,
|
|
version: metadata.version,
|
|
author: metadata.author,
|
|
source: name,
|
|
updated: new Date().toISOString(),
|
|
})
|
|
} catch {
|
|
// Skip invalid plugins
|
|
}
|
|
}
|
|
} catch {
|
|
// Marketplace might not have plugins directory
|
|
}
|
|
}
|
|
|
|
return results
|
|
}
|
|
|
|
// ============================================================================
|
|
// PLUGIN INSTALLATION
|
|
// ============================================================================
|
|
|
|
async installPlugin(
|
|
marketplace: string,
|
|
pluginName: string,
|
|
scope: 'global' | 'project' = 'global'
|
|
): Promise<InstalledPlugin> {
|
|
const marketplaces = await this.loadMarketplaces()
|
|
const mp = marketplaces[marketplace] as any
|
|
|
|
if (!mp) {
|
|
throw new Error(`Marketplace "${marketplace}" not found`)
|
|
}
|
|
|
|
const sourcePath = path.join(mp.installLocation, 'plugins', pluginName)
|
|
const pluginJsonPath = path.join(sourcePath, '.claude-plugin', 'plugin.json')
|
|
|
|
// Read plugin metadata
|
|
const metadata: PluginMetadata = JSON.parse(
|
|
await fs.readFile(pluginJsonPath, 'utf-8')
|
|
)
|
|
|
|
// Validate permissions
|
|
await this.validatePermissions(metadata.claude.permissions)
|
|
|
|
// Calculate integrity hash
|
|
const integrity = await this.calculateIntegrity(sourcePath)
|
|
|
|
// Install to cache
|
|
const versionedPath = path.join(
|
|
this.cacheDir,
|
|
marketplace,
|
|
`${pluginName}-${metadata.version}`
|
|
)
|
|
await fs.mkdir(versionedPath, { recursive: true })
|
|
|
|
await this.copyDirectory(sourcePath, versionedPath)
|
|
|
|
const installedPlugin: InstalledPlugin = {
|
|
metadata,
|
|
installPath: versionedPath,
|
|
version: metadata.version,
|
|
installedAt: new Date().toISOString(),
|
|
lastUpdated: new Date().toISOString(),
|
|
scope,
|
|
enabled: true,
|
|
integrity,
|
|
}
|
|
|
|
// Register plugin
|
|
await this.registerPlugin(installedPlugin)
|
|
|
|
// Run install script if present
|
|
const installScript = path.join(sourcePath, 'install.sh')
|
|
try {
|
|
await this.runScript(installScript, versionedPath)
|
|
} catch {
|
|
// No install script or failed
|
|
}
|
|
|
|
return installedPlugin
|
|
}
|
|
|
|
async installFromGitHub(
|
|
repo: string,
|
|
scope: 'global' | 'project' = 'global'
|
|
): Promise<InstalledPlugin> {
|
|
const [owner, name] = repo.split('/')
|
|
const tempDir = path.join(this.pluginsDir, 'tmp', `${name}-${Date.now()}`)
|
|
|
|
// Clone repository
|
|
await this.cloneRepository(`https://github.com/${repo}.git`, tempDir)
|
|
|
|
// Read plugin metadata
|
|
const pluginJsonPath = path.join(tempDir, '.claude-plugin', 'plugin.json')
|
|
const metadata: PluginMetadata = JSON.parse(
|
|
await fs.readFile(pluginJsonPath, 'utf-8')
|
|
)
|
|
|
|
// Validate and install
|
|
const integrity = await this.calculateIntegrity(tempDir)
|
|
const versionedPath = path.join(
|
|
this.cacheDir,
|
|
'github',
|
|
`${name}-${metadata.version}`
|
|
)
|
|
await fs.mkdir(versionedPath, { recursive: true })
|
|
await this.copyDirectory(tempDir, versionedPath)
|
|
|
|
const installedPlugin: InstalledPlugin = {
|
|
metadata,
|
|
installPath: versionedPath,
|
|
version: metadata.version,
|
|
installedAt: new Date().toISOString(),
|
|
lastUpdated: new Date().toISOString(),
|
|
scope,
|
|
enabled: true,
|
|
integrity,
|
|
}
|
|
|
|
await this.registerPlugin(installedPlugin)
|
|
|
|
// Cleanup temp dir
|
|
await fs.rm(tempDir, { recursive: true, force: true })
|
|
|
|
return installedPlugin
|
|
}
|
|
|
|
// ============================================================================
|
|
// PLUGIN MANAGEMENT
|
|
// ============================================================================
|
|
|
|
async uninstallPlugin(name: string, marketplace?: string): Promise<void> {
|
|
const installed = await this.loadInstalledPlugins()
|
|
|
|
const key = marketplace ? `${name}@${marketplace}` : name
|
|
|
|
if (!installed[key]) {
|
|
throw new Error(`Plugin "${name}" is not installed`)
|
|
}
|
|
|
|
const plugin = installed[key][0] as InstalledPlugin
|
|
|
|
// Run uninstall script if present
|
|
const uninstallScript = path.join(plugin.installPath, 'uninstall.sh')
|
|
try {
|
|
await this.runScript(uninstallScript, plugin.installPath)
|
|
} catch {
|
|
// No uninstall script or failed
|
|
}
|
|
|
|
// Remove from cache
|
|
await fs.rm(plugin.installPath, { recursive: true, force: true })
|
|
|
|
// Unregister
|
|
delete installed[key]
|
|
await fs.writeFile(
|
|
this.installedFile,
|
|
JSON.stringify({ version: 2, plugins: installed }, null, 2)
|
|
)
|
|
}
|
|
|
|
async enablePlugin(name: string, marketplace?: string): Promise<void> {
|
|
const installed = await this.loadInstalledPlugins()
|
|
const key = marketplace ? `${name}@${marketplace}` : name
|
|
|
|
if (installed[key]) {
|
|
installed[key][0].enabled = true
|
|
await fs.writeFile(
|
|
this.installedFile,
|
|
JSON.stringify({ version: 2, plugins: installed }, null, 2)
|
|
)
|
|
}
|
|
}
|
|
|
|
async disablePlugin(name: string, marketplace?: string): Promise<void> {
|
|
const installed = await this.loadInstalledPlugins()
|
|
const key = marketplace ? `${name}@${marketplace}` : name
|
|
|
|
if (installed[key]) {
|
|
installed[key][0].enabled = false
|
|
await fs.writeFile(
|
|
this.installedFile,
|
|
JSON.stringify({ version: 2, plugins: installed }, null, 2)
|
|
)
|
|
}
|
|
}
|
|
|
|
async updatePlugin(name: string, marketplace?: string): Promise<void> {
|
|
const installed = await this.loadInstalledPlugins()
|
|
const key = marketplace ? `${name}@${marketplace}` : name
|
|
|
|
if (!installed[key]) {
|
|
throw new Error(`Plugin "${name}" is not installed`)
|
|
}
|
|
|
|
const plugin = installed[key][0] as InstalledPlugin
|
|
|
|
// Reinstall to update
|
|
if (plugin.metadata.repository) {
|
|
await this.installFromGitHub(
|
|
plugin.metadata.repository.replace('https://github.com/', ''),
|
|
plugin.scope
|
|
)
|
|
} else {
|
|
// Reinstall from marketplace
|
|
// Implementation depends on marketplace type
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// PLUGIN LOADING
|
|
// ============================================================================
|
|
|
|
async loadInstalledPlugins(): Promise<Record<string, InstalledPlugin[]>> {
|
|
try {
|
|
const content = await fs.readFile(this.installedFile, 'utf-8')
|
|
const data = JSON.parse(content)
|
|
return data.plugins || {}
|
|
} catch {
|
|
return {}
|
|
}
|
|
}
|
|
|
|
async getEnabledPlugins(): Promise<InstalledPlugin[]> {
|
|
const installed = await this.loadInstalledPlugins()
|
|
const enabled: InstalledPlugin[] = []
|
|
|
|
for (const plugins of Object.values(installed)) {
|
|
for (const plugin of plugins) {
|
|
if (plugin.enabled) {
|
|
enabled.push(plugin as InstalledPlugin)
|
|
}
|
|
}
|
|
}
|
|
|
|
return enabled
|
|
}
|
|
|
|
// ============================================================================
|
|
// SECURITY
|
|
// ============================================================================
|
|
|
|
private async validatePermissions(permissions: string[]): Promise<void> {
|
|
const allowedPermissions = [
|
|
'read:files',
|
|
'write:files',
|
|
'execute:commands',
|
|
'network:request',
|
|
'read:config',
|
|
'write:config',
|
|
'hook:events',
|
|
]
|
|
|
|
for (const perm of permissions) {
|
|
if (!allowedPermissions.includes(perm)) {
|
|
throw new Error(`Unknown permission: ${perm}`)
|
|
}
|
|
}
|
|
}
|
|
|
|
private async calculateIntegrity(dirPath: string): Promise<string> {
|
|
const hash = createHash('sha256')
|
|
const files = await this.getAllFiles(dirPath)
|
|
|
|
for (const file of files.sort()) {
|
|
const content = await fs.readFile(file)
|
|
hash.update(content)
|
|
}
|
|
|
|
return hash.digest('hex')
|
|
}
|
|
|
|
private async getAllFiles(dirPath: string): Promise<string[]> {
|
|
const files: string[] = []
|
|
const entries = await fs.readdir(dirPath, { withFileTypes: true })
|
|
|
|
for (const entry of entries) {
|
|
const fullPath = path.join(dirPath, entry.name)
|
|
if (entry.isDirectory()) {
|
|
files.push(...(await this.getAllFiles(fullPath)))
|
|
} else {
|
|
files.push(fullPath)
|
|
}
|
|
}
|
|
|
|
return files
|
|
}
|
|
|
|
// ============================================================================
|
|
// UTILITY FUNCTIONS
|
|
// ============================================================================
|
|
|
|
private async registerPlugin(plugin: InstalledPlugin): Promise<void> {
|
|
const installed = await this.loadInstalledPlugins()
|
|
const key = `${plugin.metadata.name}@${plugin.installPath.split('/').slice(-2).join('/')}`
|
|
|
|
if (!installed[key]) {
|
|
installed[key] = []
|
|
}
|
|
|
|
installed[key].push(plugin)
|
|
|
|
await fs.writeFile(
|
|
this.installedFile,
|
|
JSON.stringify({ version: 2, plugins: installed }, null, 2)
|
|
)
|
|
}
|
|
|
|
private async cloneRepository(repoUrl: string, targetPath: string): Promise<void> {
|
|
return new Promise((resolve, reject) => {
|
|
const git = spawn('git', ['clone', '--depth', '1', repoUrl, targetPath], {
|
|
stdio: 'inherit',
|
|
})
|
|
|
|
git.on('close', (code) => {
|
|
if (code === 0) {
|
|
resolve()
|
|
} else {
|
|
reject(new Error(`Git clone failed with code ${code}`))
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
private async copyDirectory(source: string, target: string): Promise<void> {
|
|
await fs.mkdir(target, { recursive: true })
|
|
const entries = await fs.readdir(source, { withFileTypes: true })
|
|
|
|
for (const entry of entries) {
|
|
const srcPath = path.join(source, entry.name)
|
|
const destPath = path.join(target, entry.name)
|
|
|
|
if (entry.isDirectory()) {
|
|
await this.copyDirectory(srcPath, destPath)
|
|
} else {
|
|
await fs.copyFile(srcPath, destPath)
|
|
}
|
|
}
|
|
}
|
|
|
|
private async runScript(scriptPath: string, cwd: string): Promise<void> {
|
|
return new Promise((resolve, reject) => {
|
|
const shell = spawn('bash', [scriptPath], {
|
|
cwd,
|
|
stdio: 'inherit',
|
|
})
|
|
|
|
shell.on('close', (code) => {
|
|
if (code === 0) {
|
|
resolve()
|
|
} else {
|
|
reject(new Error(`Script failed with code ${code}`))
|
|
}
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// EXPORTS
|
|
// ============================================================================
|
|
|
|
export default PluginManager
|