UNPKG

@monitoro/herd

Version:

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

419 lines (352 loc) โ€ข 16.6 kB
#!/usr/bin/env node import * as process from 'process'; // Suppress all warnings in Node.js process.env.NODE_NO_WARNINGS = '1'; // This is a valid way to suppress deprecation warnings process.env.NODE_OPTIONS = '--no-deprecation --no-warnings'; import { initConfig, getHerdClient } from './config'; initConfig(); import { Command } from 'commander'; import { prompt } from 'promptly'; import { spawn, ChildProcess } from 'child_process'; import * as os from 'os'; import * as fs from 'fs'; import * as path from 'path'; import * as chokidar from 'chokidar'; import { execute as executeRun } from '../src/lib/trails/run'; import { execute as executeTest } from '../src/lib/trails/test'; import { execute as executeInit } from '../src/lib/trails/init'; import { HerdClient } from '../src/lib/HerdClient'; import { version } from '../package.json'; const program = new Command(); program .name('herd') .description('๐Ÿ‚ Herd.garden - A modern web automation platform for developers') .version( version ); // login (and save token to .herdrc) program .command('login') .description('๐Ÿ”‘ Login to Herd.garden and save your credentials') .action(async () => { if (process.env.HERD_API_KEY) { console.log('โœ… You are already logged in!'); process.exit(0); } // Ask user to enter their token const token = await prompt('๐Ÿ” Enter your Herd.garden token: '); if (!token) { console.error('โŒ Error: No token provided'); process.exit(1); } // Save token to .herdrc with proper permissions (so that we can read it later) try { // First we try to login with the token const herdClient = new HerdClient({ token }); await herdClient.initialize(); fs.writeFileSync(path.join(os.homedir(), '.herdrc'), token, { mode: 0o600 }); console.log('๐ŸŽ‰ Token saved successfully to .herdrc!'); } catch (error) { console.error('โŒ Error: Failed to save token to .herdrc', error); } finally { process.exit(1); } }); program .command('devices') .description('๐Ÿ–ฅ๏ธ List all registered devices in your herd') .action(async () => { const herdClient = getHerdClient(); console.log('๐Ÿ” Fetching your devices...'); const devices = await herdClient.listDevices(); // display device[].info as a nice table console.table(devices.map(device => ({ deviceId: device.deviceId, name: device.name, type: device.type, status: device.status, lastActive: device.lastActive ? new Date(device.lastActive).toLocaleString() : 'never', }))); console.log(`\nโœจ Found ${devices.length} device${devices.length === 1 ? '' : 's'} in your herd.\n`); }); // MCP command group const mcpCommand = program .command('mcp') .description('๐Ÿš€ MCP server management and development tools'); mcpCommand .command('dev') .description('๐Ÿ› ๏ธ Start MCP server in development mode with hot reloading') .action(async () => { try { // Get the current working directory const cwd = process.cwd(); // Read package.json to find the main file const packageJsonPath = path.join(cwd, 'package.json'); if (!fs.existsSync(packageJsonPath)) { console.error('โŒ Error: package.json not found in the current directory'); process.exit(1); } const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); const mainFile = packageJson.main; if (!mainFile) { console.error('โŒ Error: No "main" field found in package.json'); process.exit(1); } // Default ports const MCP_PORT = process.env.MCP_PORT || 4999; const INSPECTOR_PORT = process.env.INSPECTOR_PORT || 5999; let mcpProcess: ChildProcess | null = null; let inspectorProcess: ChildProcess | null = null; // Function to start the MCP server const startMcpServer = () => { if (mcpProcess) { mcpProcess.kill(); } console.log('๐Ÿš€ Starting MCP server...'); mcpProcess = spawn('npx', ['tsx', mainFile], { stdio: 'inherit', env: { ...process.env, NODE_NO_WARNINGS: '1', NODE_OPTIONS: '--no-deprecation --no-warnings' } }); mcpProcess.on('error', (err) => { console.error('โŒ Failed to start MCP server:', err); }); }; // Function to start the inspector const startInspector = () => { if (inspectorProcess) { inspectorProcess.kill(); } console.log('๐Ÿ” Starting MCP inspector...'); inspectorProcess = spawn('npx', ['@modelcontextprotocol/inspector'], { stdio: 'inherit', env: { ...process.env, PORT: INSPECTOR_PORT.toString(), NODE_NO_WARNINGS: '1', NODE_OPTIONS: '--no-deprecation --no-warnings' } }); inspectorProcess.on('error', (err) => { console.error('โŒ Failed to start MCP inspector:', err); }); // Construct the inspector URL const serverUrl = encodeURIComponent(`http://localhost:${MCP_PORT}/sse`); console.log(`\nโœจ MCP Inspector URL: http://localhost:${INSPECTOR_PORT}/?serverUrl=${serverUrl}\n`); }; // Start both processes startMcpServer(); startInspector(); // Watch for file changes const watcher = chokidar.watch(['./**/*.ts', './**/*.js'], { ignored: ['**/node_modules/**', '**/dist/**'], persistent: true }); console.log('๐Ÿ‘€ Watching for file changes...'); watcher.on('change', (path) => { console.log(`\n๐Ÿ”„ File ${path} has been changed. Restarting...`); startMcpServer(); startInspector(); }); // Handle process termination const cleanup = () => { if (mcpProcess) mcpProcess.kill(); if (inspectorProcess) inspectorProcess.kill(); console.log('\n๐Ÿ‘‹ Shutting down MCP development server...'); process.exit(0); }; process.on('SIGINT', cleanup); process.on('SIGTERM', cleanup); console.log('\n๐ŸŽ‰ MCP development environment is ready!'); } catch (error) { console.error('โŒ Error:', error); process.exit(1); } }); // Trail command group const trailCommand = program .command('trail') .description('๐Ÿ‚ Herd.garden trail management - create, run, and test your trails'); trailCommand .command('init') .description('๐Ÿ—๏ธ Create a new trail with everything you need to get started') .action(async () => { await executeInit(); }); trailCommand .command('run') .description('๐Ÿƒ Run a trail to perform automated actions on the web') .option('-t, --trail <trailPath>', 'The trail to run (defaults to current directory)') .option('-a, --action <actionName>', 'The action to run') .option('-p, --params <params>', 'The params to pass to the action') // .option('-d, --device <deviceId>', 'The device to run the trail on') .option('-o, --output-only', 'Output only the result, no colors or formatting') .action(async (options) => { const herdClient = getHerdClient(); try { const trailPath = getTrailPath(options); console.log(`๐Ÿš€ Running trail action ${options.action || 'default'} from ${path.basename(trailPath)}...`); // load the trail const result = await executeRun(herdClient, trailPath, options.action, options.params ? JSON.parse(options.params) : {}); if (options.outputOnly) { console.log(JSON.stringify(result, null, 2)); } else { // nice colorful output of result console.log("\nโœจ Result:"); console.log("\x1b[33m%s\x1b[0m", JSON.stringify(result, null, 2)); console.log("\n๐ŸŽ‰ Trail run completed successfully!"); } } catch (error) { console.error('\nโŒ Error running trail:', error); process.exit(1); } }); trailCommand .command('test') .description('๐Ÿงช Test a trail to ensure it works correctly') .option('-t, --trail <trailPath>', 'The trail to test (defaults to current directory)') .option('-s, --selector <selectorId>', 'The selector to test (either this or action must be provided)') .option('-a, --action <actionName>', 'The action to test (either this or selector must be provided)') .option('-p, --params <params>', 'The params to pass to the action (defaults to manifest params)') // .option('-d, --device <deviceId>', 'The device to test the trail on') .option('-w, --watch', 'Watch for changes in the trail folder and re-run tests') .option('-k, --keep-alive', 'Do not exit on error (defaults to true in watch mode)') .action(async (options) => { const herdClient = getHerdClient(); // Always try to register the TypeScript loader since tsx is now a direct dependency try { // Dynamically import the tsHook module const { registerTypeScriptLoader } = await import('../src/lib/trails/tsHook'); const registered = registerTypeScriptLoader(); if (registered) { console.log('โœ… TypeScript loader registered successfully'); } else { console.warn('โš ๏ธ Failed to register TypeScript loader - falling back to JavaScript files if available'); } } catch (e) { console.error('โŒ Error loading TypeScript hook module', e); } try { const trailPath = getTrailPath(options); if (options.watch) { console.log(`๐Ÿ‘€ Watching trail for changes in ${trailPath} ...`); // Watch the entire trail directory const watcher = chokidar.watch(path.resolve(trailPath), { ignored: ['**/node_modules/**', '**/dist/**'], persistent: true, ignoreInitial: true }); // Run the initial test runChildProcess(); watcher.on('change', (changedPath) => { console.log(`\n๐Ÿ”„ Changes detected in ${changedPath}. Running tests...`); console.clear(); runChildProcess(); }); function runChildProcess() { // Build the command arguments without the watch flag but with keep-alive const args = ['trail', 'test']; if (options.trail) args.push('-t', options.trail); if (options.selector) args.push('-s', options.selector); if (options.action) args.push('-a', options.action); if (options.params) args.push('-p', options.params); args.push('-k'); // Add keep-alive flag // Use the same command that was used to run this script // If we're using tsx, we need to use it for the child process too const executable = process.argv[0]; // node or other interpreter const scriptPath = process.argv[1]; // path to cli.ts // Check if we're using tsx const usingTsx = process.execArgv.some(arg => arg.includes('tsx')); let command; let commandArgs; if (usingTsx) { // If using tsx, we need to run with tsx command = executable; commandArgs = ['--import', 'tsx', scriptPath, ...args]; } else { command = executable; commandArgs = [scriptPath, ...args]; } console.log(`๐Ÿงช Running: ${command} ${commandArgs.join(' ')}`); const child = spawn(command, commandArgs, { stdio: 'inherit' }); child.on('error', (error) => { console.error(`โŒ Error running test: ${error.message}`); }); } } else { if (options.selector) { console.log(`๐Ÿงช Testing selector '${options.selector}' from ${path.basename(trailPath)}...`); } else if (options.action) { console.log(`๐Ÿงช Testing action '${options.action}' from ${path.basename(trailPath)}...`); } let result; try { if (options.action) { result = await executeTest(herdClient, trailPath, { actionName: options.action }); } else if (options.selector) { result = await executeTest(herdClient, trailPath, { selectorId: options.selector }); } else { console.error('โŒ Error: No action or selector provided'); process.exit(1); } if (result && result.status === "success") { console.log("\nโœ… Test completed successfully!"); } else if (result && result.status === "error" && !options.keepAlive) { console.error("\nโŒ Test failed!"); process.exit(1); } } catch (err: any) { // Enhanced error handling if (err.message && err.message.includes('Unexpected token')) { console.error(` โŒ TypeScript loading error: Unable to parse TypeScript files directly. Please either: 1. Install tsx globally: npm install -g tsx 2. Run with the --use-tsx flag: herd trail test --use-tsx -a ${options.action || options.selector} 3. Compile your TypeScript files to JavaScript first: tsc Original error: ${err.message} `); } else { console.error('\nโŒ Error testing trail:', err); } if (!options.keepAlive) { process.exit(1); } } } } catch (error) { console.error('โŒ Error testing trail:', error); if (!options.keepAlive) { process.exit(1); } } }); // Let's also update the getTrailPath function to have nicer error messages function getTrailPath(options: any) { let trailPath = process.cwd(); // Did we get a trail name? pick this one otherwise try current directory if (options.trail) { trailPath = path.join(process.cwd(), options.trail); } // Check for both TS and JS files const hasTypeScriptFiles = fs.existsSync(path.join(trailPath, "actions.ts")) && fs.existsSync(path.join(trailPath, "urls.ts")) && fs.existsSync(path.join(trailPath, "selectors.ts")); const hasJavaScriptFiles = fs.existsSync(path.join(trailPath, "actions.js")) && fs.existsSync(path.join(trailPath, "urls.js")) && fs.existsSync(path.join(trailPath, "selectors.js")); // are we inside a trail directory? if (!hasTypeScriptFiles && !hasJavaScriptFiles) { console.error('โŒ No valid trail found! Missing required files (actions, urls, selectors)'); console.log('๐Ÿ’ก Tip: Run "herd trail init" to create a new trail, or cd into an existing trail directory'); process.exit(1); } return trailPath; } program.parse(process.argv);