@monitoro/herd
Version:
Automate your browser, build AI web tools and MCP servers with Monitoro Herd
148 lines (147 loc) • 5.89 kB
JavaScript
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();
}