UNPKG

chromancer

Version:

A powerful command-line interface for automating Chrome browser using Playwright. Perfect for web scraping, automation, testing, and browser workflows.

334 lines (333 loc) • 13.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = require("tslib"); const core_1 = require("@oclif/core"); const base_js_1 = require("../base.js"); const fs_1 = require("fs"); const yaml = tslib_1.__importStar(require("yaml")); // Create a chalk-like interface for colorization const chalk = { red: (s) => `\x1b[31m${s}\x1b[0m`, yellow: (s) => `\x1b[33m${s}\x1b[0m`, gray: (s) => `\x1b[90m${s}\x1b[0m`, blue: (s) => `\x1b[34m${s}\x1b[0m`, green: (s) => `\x1b[32m${s}\x1b[0m`, cyan: (s) => `\x1b[36m${s}\x1b[0m`, }; class Run extends base_js_1.BaseCommand { parsedFlags; static description = 'Execute powerful YAML workflows with variables, conditional logic, and error handling - automate complex multi-step browser operations'; static examples = [ '<%= config.bin %> <%= command.id %> workflow.yml', '<%= config.bin %> <%= command.id %> --file automation.yml --strict', 'echo "navigate: https://example.com\\nwait: {selector: body}" | <%= config.bin %> <%= command.id %>', '<%= config.bin %> <%= command.id %> test.yml --var USER=john --var PASS=secret', ]; static args = { file: core_1.Args.string({ description: 'YAML workflow file to run', required: false, }), }; static flags = { ...base_js_1.BaseCommand.baseFlags, strict: core_1.Flags.boolean({ description: 'Stop on first error (default: true)', default: true, }), 'continue-on-error': core_1.Flags.boolean({ description: 'Continue execution even if a step fails', default: false, }), var: core_1.Flags.string({ description: 'Set workflow variables (KEY=VALUE)', multiple: true, }), timeout: core_1.Flags.integer({ description: 'Default timeout for commands in milliseconds', default: 30000, }), 'dry-run': core_1.Flags.boolean({ description: 'Parse and validate workflow without executing', default: false, }), }; variables = {}; stepCount = 0; successCount = 0; failureCount = 0; async run() { const { args, flags } = await this.parse(Run); this.parsedFlags = flags; // Parse variables if (flags.var) { for (const varDef of flags.var) { const [key, ...valueParts] = varDef.split('='); if (key && valueParts.length > 0) { this.variables[key] = valueParts.join('='); } } } // Load workflow let workflowContent; if (args.file) { // Read from file workflowContent = await fs_1.promises.readFile(args.file, 'utf-8'); this.log(chalk.cyan(`šŸ“‹ Loading workflow from: ${args.file}`)); } else if (process.stdin.isTTY === false) { // Read from stdin workflowContent = await this.readStdin(); this.log(chalk.cyan('šŸ“‹ Loading workflow from stdin')); } else { this.error('No workflow file specified. Provide a file path or pipe content to stdin.'); } // Parse YAML let workflow; try { const parsed = yaml.parse(workflowContent); workflow = Array.isArray(parsed) ? parsed : [parsed]; } catch (error) { this.error(`Failed to parse YAML: ${error.message}`); } if (flags['dry-run']) { this.log(chalk.yellow('šŸ” Dry run mode - validating workflow')); this.validateWorkflow(workflow); this.log(chalk.green('āœ… Workflow is valid')); return; } // Connect to Chrome await this.connectToChrome(flags.port, flags.host, flags.launch, flags.profile, flags.headless, flags.verbose, flags.keepOpen); if (!this.page) { this.error('Failed to connect to Chrome'); } // Execute workflow const options = { strict: flags.strict && !flags['continue-on-error'], variables: this.variables, timeout: flags.timeout, }; await this.executeWorkflow(workflow, this.page, options); // Show summary this.showSummary(); } async readStdin() { const chunks = []; for await (const chunk of process.stdin) { chunks.push(chunk); } return Buffer.concat(chunks).toString('utf-8'); } validateWorkflow(workflow) { if (!Array.isArray(workflow) || workflow.length === 0) { this.error('Workflow must contain at least one step'); } workflow.forEach((step, index) => { if (typeof step !== 'object' || step === null) { this.error(`Step ${index + 1} must be an object`); } const commands = Object.keys(step); if (commands.length === 0) { this.error(`Step ${index + 1} must contain a command`); } if (commands.length > 1) { this.warn(`Step ${index + 1} contains multiple commands - only first will be executed`); } }); } async executeWorkflow(workflow, page, options) { this.log(chalk.cyan(`\nšŸš€ Executing workflow with ${workflow.length} steps\n`)); for (const [index, step] of workflow.entries()) { this.stepCount++; const stepNumber = index + 1; // Get command and args const command = Object.keys(step)[0]; let args = step[command]; // Replace variables args = this.replaceVariables(args, options.variables || {}); this.log(chalk.blue(`[${stepNumber}/${workflow.length}] ${command}`)); try { await this.executeStep(command, args, page, options); this.successCount++; this.log(chalk.green(` āœ… Success`)); } catch (error) { this.failureCount++; this.log(chalk.red(` āŒ Failed: ${error.message}`)); if (options.strict) { this.error(`Workflow failed at step ${stepNumber}: ${error.message}`); } } // Add small delay between steps await page.waitForTimeout(100); } } async executeStep(command, args, page, options) { const timeout = options.timeout || 30000; switch (command) { case 'navigate': case 'goto': const url = typeof args === 'string' ? args : args.url; await page.goto(url, { waitUntil: args.waitUntil || 'load', timeout }); break; case 'click': const clickSelector = typeof args === 'string' ? args : args.selector; await page.click(clickSelector, { button: args.button || 'left', clickCount: args.clickCount || 1, timeout, }); break; case 'type': const typeSelector = typeof args === 'string' ? args.split(' ')[0] : args.selector; const text = typeof args === 'string' ? args.split(' ').slice(1).join(' ') : args.text; await page.type(typeSelector, text, { delay: args.delay || 0, }); if (args.submit || args.enter) { await page.press(typeSelector, 'Enter'); } break; case 'wait': if (typeof args === 'string') { await page.waitForSelector(args, { state: 'visible', timeout }); } else if (args.selector) { await page.waitForSelector(args.selector, { state: args.state || 'visible', timeout: args.timeout || timeout, }); } else if (args.time || args.ms) { await page.waitForTimeout(args.time || args.ms); } else if (args.url) { await page.waitForURL(args.url, { timeout }); } break; case 'screenshot': const path = typeof args === 'string' ? args : args.path; await page.screenshot({ path, fullPage: args.fullPage !== false, type: args.type || 'png', }); break; case 'evaluate': case 'eval': const code = typeof args === 'string' ? args : args.code; const result = await page.evaluate(code); if (this.parsedFlags.verbose) { this.log(chalk.gray(` Result: ${JSON.stringify(result)}`)); } break; case 'select': const selectSelector = typeof args === 'string' ? args.split(' ')[0] : args.selector; const value = typeof args === 'string' ? args.split(' ').slice(1).join(' ') : args.value; await page.selectOption(selectSelector, value); break; case 'hover': const hoverSelector = typeof args === 'string' ? args : args.selector; await page.hover(hoverSelector, { timeout, position: args.position, }); break; case 'scroll': if (typeof args === 'string') { await page.evaluate(`window.scrollTo(0, document.body.scrollHeight)`); } else if (args.to) { await page.evaluate(({ selector }) => { document.querySelector(selector)?.scrollIntoView({ behavior: 'smooth' }); }, { selector: args.to }); } else if (args.by) { await page.evaluate(({ pixels }) => { window.scrollBy(0, pixels); }, { pixels: args.by }); } break; case 'fill': if (typeof args === 'object' && args.form) { for (const [field, value] of Object.entries(args.form)) { await page.fill(`[name="${field}"]`, String(value)); } } break; case 'press': const key = typeof args === 'string' ? args : args.key; await page.keyboard.press(key); break; case 'reload': case 'refresh': await page.reload({ waitUntil: args.waitUntil || 'load' }); break; case 'back': await page.goBack({ waitUntil: args.waitUntil || 'load' }); break; case 'forward': await page.goForward({ waitUntil: args.waitUntil || 'load' }); break; default: throw new Error(`Unknown command: ${command}`); } } replaceVariables(value, variables) { if (typeof value === 'string') { // Replace ${VAR} patterns return value.replace(/\$\{(\w+)\}/g, (match, varName) => { return variables[varName] || match; }); } else if (Array.isArray(value)) { return value.map(item => this.replaceVariables(item, variables)); } else if (typeof value === 'object' && value !== null) { const result = {}; for (const [key, val] of Object.entries(value)) { result[key] = this.replaceVariables(val, variables); } return result; } return value; } showSummary() { this.log(''); this.log(chalk.cyan('šŸ“Š Workflow Summary:')); this.log(` Total steps: ${this.stepCount}`); this.log(` ${chalk.green('Successful:')} ${this.successCount}`); if (this.failureCount > 0) { this.log(` ${chalk.red('Failed:')} ${this.failureCount}`); } const successRate = this.stepCount > 0 ? Math.round((this.successCount / this.stepCount) * 100) : 0; if (successRate === 100) { this.log(chalk.green(`\nāœ… All steps completed successfully!`)); } else if (successRate > 0) { this.log(chalk.yellow(`\nāš ļø ${successRate}% success rate`)); } else { this.log(chalk.red(`\nāŒ Workflow failed`)); } } } exports.default = Run;