- Remove top-level Node.js imports that break bundlers - Use universal adapters for crypto operations - Add dynamic imports for Node.js-specific modules - Add browser field to package.json for bundler hints - Maintain full Node.js functionality while enabling browser usage This allows Brainy to be used with modern bundlers (Vite, Webpack, etc.) without requiring Node.js polyfills. Browser environments get core features while Node.js retains all capabilities including filesystem and networking.
315 lines
No EOL
9.5 KiB
TypeScript
315 lines
No EOL
9.5 KiB
TypeScript
/**
|
|
* Local Augmentation Discovery
|
|
*
|
|
* Discovers augmentations installed locally in node_modules
|
|
* and built-in augmentations that ship with Brainy
|
|
*
|
|
* NOTE: This is a Node.js-only feature that requires filesystem access
|
|
*/
|
|
|
|
import { AugmentationManifest } from '../manifest.js'
|
|
|
|
// Node.js modules - dynamically imported to avoid bundler issues
|
|
let fs: any = null
|
|
let path: any = null
|
|
|
|
// Load Node.js modules if available
|
|
if (typeof window === 'undefined') {
|
|
try {
|
|
fs = await import('node:fs')
|
|
path = await import('node:path')
|
|
} catch (e) {
|
|
// Will throw error in methods if not available
|
|
}
|
|
}
|
|
|
|
// Create compatibility layer for sync methods
|
|
const existsSync = fs?.existsSync || (() => { throw new Error('Filesystem not available') })
|
|
const readdirSync = fs?.readdirSync || (() => { throw new Error('Filesystem not available') })
|
|
const readFileSync = fs?.readFileSync || (() => { throw new Error('Filesystem not available') })
|
|
const join = path?.join || ((...args: string[]) => args.join('/'))
|
|
|
|
export interface LocalAugmentation {
|
|
id: string
|
|
name: string
|
|
source: 'builtin' | 'npm' | 'local'
|
|
path: string
|
|
manifest?: AugmentationManifest
|
|
package?: {
|
|
name: string
|
|
version: string
|
|
description?: string
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Discovers augmentations installed locally
|
|
*/
|
|
export class LocalAugmentationDiscovery {
|
|
private builtInAugmentations: Map<string, LocalAugmentation> = new Map()
|
|
private installedAugmentations: Map<string, LocalAugmentation> = new Map()
|
|
|
|
constructor(private options: {
|
|
brainyPath?: string
|
|
projectPath?: string
|
|
scanNodeModules?: boolean
|
|
} = {}) {
|
|
this.options = {
|
|
brainyPath: this.options.brainyPath || this.findBrainyPath(),
|
|
projectPath: this.options.projectPath || process.cwd(),
|
|
scanNodeModules: this.options.scanNodeModules ?? true
|
|
}
|
|
|
|
// Register built-in augmentations
|
|
this.registerBuiltIn()
|
|
}
|
|
|
|
/**
|
|
* Register built-in augmentations that ship with Brainy
|
|
*/
|
|
private registerBuiltIn(): void {
|
|
const builtIn = [
|
|
{ id: 'cache', name: 'Cache', path: 'cacheAugmentation' },
|
|
{ id: 'batch', name: 'Batch Processing', path: 'batchProcessingAugmentation' },
|
|
{ id: 'entity-registry', name: 'Entity Registry', path: 'entityRegistryAugmentation' },
|
|
{ id: 'index', name: 'Index', path: 'indexAugmentation' },
|
|
{ id: 'metrics', name: 'Metrics', path: 'metricsAugmentation' },
|
|
{ id: 'monitoring', name: 'Monitoring', path: 'monitoringAugmentation' },
|
|
{ id: 'connection-pool', name: 'Connection Pool', path: 'connectionPoolAugmentation' },
|
|
{ id: 'request-deduplicator', name: 'Request Deduplicator', path: 'requestDeduplicatorAugmentation' },
|
|
{ id: 'api-server', name: 'API Server', path: 'apiServerAugmentation' },
|
|
{ id: 'neural-import', name: 'Neural Import', path: 'neuralImport' },
|
|
{ id: 'intelligent-verb-scoring', name: 'Intelligent Verb Scoring', path: 'intelligentVerbScoringAugmentation' },
|
|
{ id: 'universal-display', name: 'Universal Display', path: 'universalDisplayAugmentation' },
|
|
// Storage augmentations
|
|
{ id: 'memory-storage', name: 'Memory Storage', path: 'storageAugmentations', export: 'MemoryStorageAugmentation' },
|
|
{ id: 'filesystem-storage', name: 'FileSystem Storage', path: 'storageAugmentations', export: 'FileSystemStorageAugmentation' },
|
|
{ id: 'opfs-storage', name: 'OPFS Storage', path: 'storageAugmentations', export: 'OPFSStorageAugmentation' },
|
|
{ id: 's3-storage', name: 'S3 Storage', path: 'storageAugmentations', export: 'S3StorageAugmentation' },
|
|
]
|
|
|
|
for (const aug of builtIn) {
|
|
this.builtInAugmentations.set(aug.id, {
|
|
id: aug.id,
|
|
name: aug.name,
|
|
source: 'builtin',
|
|
path: `@soulcraft/brainy/augmentations/${aug.path}`,
|
|
package: {
|
|
name: '@soulcraft/brainy',
|
|
version: 'builtin',
|
|
description: `Built-in ${aug.name} augmentation`
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Find Brainy installation path
|
|
*/
|
|
private findBrainyPath(): string {
|
|
// Try to find brainy in node_modules
|
|
const possiblePaths = [
|
|
join(process.cwd(), 'node_modules', '@soulcraft', 'brainy'),
|
|
join(process.cwd(), 'node_modules', 'brainy'),
|
|
join(process.cwd(), '..', 'node_modules', '@soulcraft', 'brainy'),
|
|
]
|
|
|
|
for (const path of possiblePaths) {
|
|
if (existsSync(path)) {
|
|
return path
|
|
}
|
|
}
|
|
|
|
// Fallback to current directory
|
|
return process.cwd()
|
|
}
|
|
|
|
/**
|
|
* Discover all augmentations
|
|
*/
|
|
async discoverAll(): Promise<LocalAugmentation[]> {
|
|
const augmentations: LocalAugmentation[] = []
|
|
|
|
// Add built-in augmentations
|
|
augmentations.push(...this.builtInAugmentations.values())
|
|
|
|
// Scan node_modules if enabled
|
|
if (this.options.scanNodeModules) {
|
|
const installed = await this.scanNodeModules()
|
|
augmentations.push(...installed)
|
|
}
|
|
|
|
// Scan local project
|
|
const local = await this.scanLocalProject()
|
|
augmentations.push(...local)
|
|
|
|
return augmentations
|
|
}
|
|
|
|
/**
|
|
* Scan node_modules for installed augmentations
|
|
*/
|
|
private async scanNodeModules(): Promise<LocalAugmentation[]> {
|
|
const augmentations: LocalAugmentation[] = []
|
|
const nodeModulesPath = join(this.options.projectPath!, 'node_modules')
|
|
|
|
if (!existsSync(nodeModulesPath)) {
|
|
return augmentations
|
|
}
|
|
|
|
// Scan @brainy/* packages
|
|
const brainyPath = join(nodeModulesPath, '@brainy')
|
|
if (existsSync(brainyPath)) {
|
|
const packages = readdirSync(brainyPath)
|
|
for (const pkg of packages) {
|
|
const augmentation = await this.loadPackageAugmentation(join(brainyPath, pkg))
|
|
if (augmentation) {
|
|
augmentations.push(augmentation)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Scan packages with brainy-augmentation keyword
|
|
const packages = readdirSync(nodeModulesPath)
|
|
for (const pkg of packages) {
|
|
if (pkg.startsWith('@') || pkg.startsWith('.')) continue
|
|
|
|
const pkgPath = join(nodeModulesPath, pkg)
|
|
const packageJson = this.loadPackageJson(pkgPath)
|
|
|
|
if (packageJson?.keywords?.includes('brainy-augmentation')) {
|
|
const augmentation = await this.loadPackageAugmentation(pkgPath)
|
|
if (augmentation) {
|
|
augmentations.push(augmentation)
|
|
}
|
|
}
|
|
}
|
|
|
|
return augmentations
|
|
}
|
|
|
|
/**
|
|
* Scan local project for augmentations
|
|
*/
|
|
private async scanLocalProject(): Promise<LocalAugmentation[]> {
|
|
const augmentations: LocalAugmentation[] = []
|
|
|
|
// Check for augmentations directory
|
|
const augPath = join(this.options.projectPath!, 'augmentations')
|
|
if (existsSync(augPath)) {
|
|
const files = readdirSync(augPath)
|
|
for (const file of files) {
|
|
if (file.endsWith('.ts') || file.endsWith('.js')) {
|
|
const name = file.replace(/\.(ts|js)$/, '')
|
|
augmentations.push({
|
|
id: name,
|
|
name: this.humanizeName(name),
|
|
source: 'local',
|
|
path: join(augPath, file)
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
return augmentations
|
|
}
|
|
|
|
/**
|
|
* Load augmentation from package
|
|
*/
|
|
private async loadPackageAugmentation(pkgPath: string): Promise<LocalAugmentation | null> {
|
|
const packageJson = this.loadPackageJson(pkgPath)
|
|
if (!packageJson) return null
|
|
|
|
// Check if it's a brainy augmentation
|
|
const isBrainyAug = packageJson.keywords?.includes('brainy-augmentation') ||
|
|
packageJson.brainy?.type === 'augmentation'
|
|
|
|
if (!isBrainyAug) return null
|
|
|
|
const manifest = packageJson.brainy?.manifest || null
|
|
|
|
return {
|
|
id: packageJson.brainy?.id || packageJson.name.replace('@brainy/', ''),
|
|
name: packageJson.brainy?.name || packageJson.name,
|
|
source: 'npm',
|
|
path: pkgPath,
|
|
manifest,
|
|
package: {
|
|
name: packageJson.name,
|
|
version: packageJson.version,
|
|
description: packageJson.description
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load package.json
|
|
*/
|
|
private loadPackageJson(pkgPath: string): any {
|
|
const packageJsonPath = join(pkgPath, 'package.json')
|
|
if (!existsSync(packageJsonPath)) return null
|
|
|
|
try {
|
|
return JSON.parse(readFileSync(packageJsonPath, 'utf8'))
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Convert name to human-readable format
|
|
*/
|
|
private humanizeName(name: string): string {
|
|
return name
|
|
.replace(/[-_]/g, ' ')
|
|
.replace(/augmentation/gi, '')
|
|
.replace(/\b\w/g, l => l.toUpperCase())
|
|
.trim()
|
|
}
|
|
|
|
/**
|
|
* Get built-in augmentations
|
|
*/
|
|
getBuiltIn(): LocalAugmentation[] {
|
|
return Array.from(this.builtInAugmentations.values())
|
|
}
|
|
|
|
/**
|
|
* Get installed augmentations
|
|
*/
|
|
getInstalled(): LocalAugmentation[] {
|
|
return Array.from(this.installedAugmentations.values())
|
|
}
|
|
|
|
/**
|
|
* Check if augmentation is installed
|
|
*/
|
|
isInstalled(id: string): boolean {
|
|
return this.builtInAugmentations.has(id) ||
|
|
this.installedAugmentations.has(id)
|
|
}
|
|
|
|
/**
|
|
* Get import path for augmentation
|
|
*/
|
|
getImportPath(id: string): string | null {
|
|
const aug = this.builtInAugmentations.get(id) ||
|
|
this.installedAugmentations.get(id)
|
|
return aug?.path || null
|
|
}
|
|
|
|
/**
|
|
* Load augmentation module dynamically
|
|
*/
|
|
async loadAugmentation(id: string): Promise<any> {
|
|
const path = this.getImportPath(id)
|
|
if (!path) {
|
|
throw new Error(`Augmentation ${id} not found`)
|
|
}
|
|
|
|
// Dynamic import
|
|
const module = await import(path)
|
|
return module.default || module
|
|
}
|
|
} |