UNPKG

@monitoro/herd

Version:

Automate your browser, build AI web tools and MCP servers with Monitoro Herd

148 lines (147 loc) 5.89 kB
import fs from 'fs/promises'; import path from 'path'; import os from 'os'; import { CacheManager } from './CacheManager.js'; /** * Download a trail from the registry with secure caching * @param client HerdClient instance * @param trailName Trail name in format 'organization/trail' * @param options Download options * @returns Path to the downloaded trail */ export async function downloadTrail(client, trailName, options = {}) { const { cacheDirectory = path.join(os.homedir(), '.cache', 'herd', 'trails'), version = 'latest', cacheEnabled = true, silent = false } = options; if (!trailName.includes('/')) { throw new Error('Trail name must be in format "organization/trail"'); } const [org, name] = trailName.split('/'); // Create cache manager const cacheManager = new CacheManager(client, { cacheDirectory }); // Create trail directory path (using temp dir for encrypted cache) const trailDir = path.join(os.tmpdir(), 'herd-trails', `${Date.now()}-${Math.random().toString(36).slice(2)}`); // Cache key for this trail+version const cacheKey = `trail:${trailName}:${version}`; // Check if this trail is cached let isCached = false; if (cacheEnabled) { isCached = await cacheManager.exists(cacheKey); } // Handle cached trail if (isCached && cacheEnabled) { if (!silent) { console.log(`ℹ️ Using cached trail: ${trailName}@${version}`); } // Ensure the directory exists await fs.mkdir(trailDir, { recursive: true }); try { // Get cached trail data const cachedData = await cacheManager.retrieve(cacheKey); if (cachedData) { // Parse the cached data const trailCache = JSON.parse(cachedData); // Write files to the temporary directory await Promise.all([ fs.writeFile(path.join(trailDir, 'package.json'), trailCache.packageJson), fs.writeFile(path.join(trailDir, 'urls.js'), trailCache.urls), fs.writeFile(path.join(trailDir, 'selectors.js'), trailCache.selectors), fs.writeFile(path.join(trailDir, 'actions.js'), trailCache.actions), fs.writeFile(path.join(trailDir, 'manifest.json'), trailCache.manifest), ]); return trailDir; } } catch (error) { // Cache error - proceed to download if (!silent) { console.warn('⚠️ Cache error, downloading fresh copy'); } } } // Download the trail if (!silent) { console.log(`🔄 Downloading trail: ${trailName}@${version}`); } // Ensure directory exists await fs.mkdir(trailDir, { recursive: true }); try { // Construct API endpoints const prefix = `/api/registry/trail/${org.replace('@', '')}/${name}`; const versionSuffix = version === 'latest' ? '' : `/${version}`; // Fetch trail assets in parallel const [metadata, manifest, urls, selectors, actions] = await Promise.all([ fetchJson(client, `${prefix}`), fetchJson(client, `${prefix}/manifest${versionSuffix}`), fetchText(client, `${prefix}/urls${versionSuffix}`), fetchText(client, `${prefix}/selectors${versionSuffix}`), fetchText(client, `${prefix}/actions${versionSuffix}`) ]); // Create package.json content const packageJson = JSON.stringify({ name: `${org}-${name}`, version: metadata.latestVersion || version, description: metadata.description || '', herdTrail: { organizationId: metadata.organization?.id, }, keywords: metadata.tags || [] }, null, 2); // Write files to disk await Promise.all([ fs.writeFile(path.join(trailDir, 'package.json'), packageJson), fs.writeFile(path.join(trailDir, 'urls.js'), urls), fs.writeFile(path.join(trailDir, 'selectors.js'), selectors), fs.writeFile(path.join(trailDir, 'actions.js'), actions), fs.writeFile(path.join(trailDir, 'manifest.json'), JSON.stringify(manifest, null, 2)) ]); // Cache the trail if caching is enabled if (cacheEnabled) { try { const cacheData = JSON.stringify({ packageJson, urls, selectors, actions, manifest: JSON.stringify(manifest, null, 2), timestamp: Date.now() }); await cacheManager.store(cacheKey, cacheData); } catch (error) { // Ignore cache errors if (!silent) { console.warn('⚠️ Failed to cache trail data'); } } } if (!silent) { console.log(`✅ Downloaded trail: ${trailName}@${version}`); } return trailDir; } catch (error) { if (!silent) { console.error(`❌ Failed to download trail ${trailName}@${version}:`, error); } throw error; } } /** * Fetch JSON from an API endpoint */ async function fetchJson(client, endpoint) { const response = await client.request(endpoint); if (!response.ok) { throw new Error(`Failed to fetch ${endpoint}: ${response.statusText}`); } return response.json(); } /** * Fetch text from an API endpoint */ async function fetchText(client, endpoint) { const response = await client.request(endpoint); if (!response.ok) { throw new Error(`Failed to fetch ${endpoint}: ${response.statusText}`); } return response.text(); }