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) 14.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 workflow_storage_js_1 = require("../utils/workflow-storage.js"); const workflow_executor_js_1 = require("../utils/workflow-executor.js"); const yaml = tslib_1.__importStar(require("yaml")); const fs = tslib_1.__importStar(require("node:fs/promises")); const prompts_1 = require("@inquirer/prompts"); // Create a chalk-like interface for colorization const chalk = { green: (s) => `\x1b[32m${s}\x1b[0m`, red: (s) => `\x1b[31m${s}\x1b[0m`, yellow: (s) => `\x1b[33m${s}\x1b[0m`, blue: (s) => `\x1b[34m${s}\x1b[0m`, gray: (s) => `\x1b[90m${s}\x1b[0m`, cyan: (s) => `\x1b[36m${s}\x1b[0m`, bold: (s) => `\x1b[1m${s}\x1b[0m`, }; class Workflows extends base_js_1.BaseCommand { static description = 'Manage saved Chromancer workflows'; async stabilizeTerminal() { // Since we're using @inquirer/prompts everywhere, we just need minimal cleanup // Inquirer handles all the terminal state management for us // Ensure cursor is visible process.stdout.write('\x1b[?25h'); // Clear any residual line content process.stdout.write('\r\x1b[K'); // Small delay to let any pending output complete await new Promise(resolve => setTimeout(resolve, 10)); } static examples = [ '<%= config.bin %> <%= command.id %> list', '<%= config.bin %> <%= command.id %> run "login flow"', '<%= config.bin %> <%= command.id %> show "scrape data"', '<%= config.bin %> <%= command.id %> delete "old workflow"', '<%= config.bin %> <%= command.id %> export "my workflow" --output workflow.yml', ]; static args = { action: core_1.Args.string({ description: 'Action to perform', options: ['list', 'run', 'show', 'delete', 'export', 'import'], required: false, }), name: core_1.Args.string({ description: 'Workflow name or ID', required: false, }), }; static flags = { ...base_js_1.BaseCommand.baseFlags, tags: core_1.Flags.string({ description: 'Filter by tags (comma-separated)', multiple: false, }), output: core_1.Flags.string({ char: 'o', description: 'Output file for export', }), file: core_1.Flags.string({ char: 'f', description: 'Input file for import', }), }; storage = new workflow_storage_js_1.WorkflowStorage(); async run() { const { args, flags } = await this.parse(Workflows); // If no action specified, show interactive menu if (!args.action) { await this.showInteractiveMenu(flags); return; } switch (args.action) { case 'list': await this.listWorkflows(flags); break; case 'run': if (!args.name) this.error('Workflow name or ID required'); await this.runWorkflow(args.name, flags); break; case 'show': if (!args.name) this.error('Workflow name or ID required'); await this.showWorkflow(args.name); break; case 'delete': if (!args.name) this.error('Workflow name or ID required'); await this.deleteWorkflow(args.name); break; case 'export': if (!args.name) this.error('Workflow name or ID required'); await this.exportWorkflow(args.name, flags); break; case 'import': await this.importWorkflow(flags); break; default: this.error(`Unknown action: ${args.action}`); } } async showInteractiveMenu(flags) { const workflows = await this.storage.list(); if (workflows.length === 0) { this.log('No workflows saved yet.'); this.log('\nCreate workflows using: chromancer claude "your instruction"'); return; } const choices = [ ...workflows.map(w => ({ name: `${w.name} ${chalk.gray(`(${w.tags?.join(', ') || 'no tags'}) - ${w.executions || 0} runs`)}`, value: { action: 'run', workflow: w } })), { name: '─────────────────', value: 'separator', disabled: true }, { name: '📋 List all workflows', value: { action: 'list' } }, { name: '📥 Import workflow', value: { action: 'import' } }, { name: '🚪 Exit', value: { action: 'exit' } } ]; await this.stabilizeTerminal(); const selection = await (0, prompts_1.select)({ message: 'Select a workflow to run or an action:', choices: choices.map(c => ({ name: c.name, value: c.value })) }); // Handle separator selection if (typeof selection === 'string' && selection === 'separator') { // Re-run the menu if separator was selected return this.showInteractiveMenu(flags); } // TypeScript type guard if (typeof selection === 'object' && 'action' in selection) { switch (selection.action) { case 'run': if ('workflow' in selection) { await this.runWorkflow(selection.workflow.id, flags); } break; case 'list': await this.listWorkflows(flags); break; case 'import': await this.importWorkflow(flags); break; case 'exit': break; } } } async listWorkflows(flags) { const filter = {}; if (flags.tags) { filter.tags = flags.tags.split(',').map((t) => t.trim()); } const workflows = await this.storage.list(filter); if (workflows.length === 0) { this.log('No workflows found.'); return; } this.log(chalk.bold('\n📚 Saved Workflows:\n')); for (const workflow of workflows) { this.log(chalk.green(`▸ ${workflow.name}`)); if (workflow.description) { this.log(chalk.gray(` ${workflow.description}`)); } this.log(chalk.gray(` ID: ${workflow.id}`)); this.log(chalk.gray(` Created: ${new Date(workflow.createdAt).toLocaleString()}`)); if (workflow.tags && workflow.tags.length > 0) { this.log(chalk.gray(` Tags: ${workflow.tags.join(', ')}`)); } if (workflow.executions) { this.log(chalk.gray(` Executions: ${workflow.executions}`)); } this.log(''); } } async runWorkflow(nameOrId, flags) { try { const workflow = await this.storage.load(nameOrId); this.log(chalk.cyan(`\n🚀 Running workflow: ${workflow.name}\n`)); // 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'); } // Parse and execute workflow const workflowSteps = yaml.parse(workflow.yaml); const executor = new workflow_executor_js_1.WorkflowExecutor(this.page); const result = await executor.execute(workflowSteps, { strict: true, captureOutput: flags.verbose }); // Show results this.log(chalk.cyan('\n📊 Execution Summary:')); this.log(` Total steps: ${result.totalSteps}`); this.log(` ${chalk.green('✅ Successful:')} ${result.successfulSteps}`); if (result.failedSteps > 0) { this.log(` ${chalk.red('❌ Failed:')} ${result.failedSteps}`); } this.log(` ⏱️ Duration: ${result.totalDuration}ms`); // Record execution await this.storage.recordExecution(workflow.id); if (result.success) { this.log(chalk.green('\n✅ Workflow completed successfully!')); } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); this.error(`Failed to run workflow: ${errorMessage}`); } } async showWorkflow(nameOrId) { try { const workflow = await this.storage.load(nameOrId); this.log(chalk.bold(`\n📄 Workflow: ${workflow.name}\n`)); if (workflow.description) { this.log(`Description: ${workflow.description}`); } this.log(`\nOriginal prompt: "${workflow.prompt}"`); this.log(`Created: ${new Date(workflow.createdAt).toLocaleString()}`); this.log(`Updated: ${new Date(workflow.updatedAt).toLocaleString()}`); if (workflow.tags && workflow.tags.length > 0) { this.log(`Tags: ${workflow.tags.join(', ')}`); } this.log(`Executions: ${workflow.executions || 0}`); if (workflow.lastExecuted) { this.log(`Last executed: ${new Date(workflow.lastExecuted).toLocaleString()}`); } this.log(chalk.cyan('\nWorkflow YAML:')); this.log('---'); this.log(workflow.yaml); this.log('---'); if (workflow.versions && workflow.versions.length > 1) { this.log(chalk.yellow(`\n📝 Version History (${workflow.versions.length} versions):`)); workflow.versions.forEach((v, i) => { this.log(` v${v.version} - ${new Date(v.createdAt).toLocaleString()} - ${v.reason}`); }); } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); this.error(`Failed to show workflow: ${errorMessage}`); } } async deleteWorkflow(nameOrId) { try { const workflow = await this.storage.load(nameOrId); await this.stabilizeTerminal(); const shouldDelete = await (0, prompts_1.confirm)({ message: `Are you sure you want to delete "${workflow.name}"?`, default: false }); if (!shouldDelete) { this.log('Deletion cancelled.'); return; } await this.storage.delete(workflow.id); this.log(chalk.green(`✅ Workflow "${workflow.name}" deleted.`)); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); this.error(`Failed to delete workflow: ${errorMessage}`); } } async exportWorkflow(nameOrId, flags) { try { const workflow = await this.storage.load(nameOrId); const outputFile = flags.output || `${workflow.name.replace(/\s+/g, '-')}.yml`; await fs.writeFile(outputFile, workflow.yaml); this.log(chalk.green(`✅ Workflow exported to: ${outputFile}`)); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); this.error(`Failed to export workflow: ${errorMessage}`); } } async importWorkflow(flags) { const file = flags.file || await this.promptForFile(); try { const yamlContent = await fs.readFile(file, 'utf-8'); // Validate YAML const parsed = yaml.parse(yamlContent); if (!Array.isArray(parsed)) { this.error('Invalid workflow: must be an array of steps'); } // Prompt for workflow details await this.stabilizeTerminal(); const name = await (0, prompts_1.input)({ message: 'Workflow name:', validate: (value) => value.length > 0 || 'Name is required' }); await this.stabilizeTerminal(); const prompt = await (0, prompts_1.input)({ message: 'Original instruction/prompt:', default: 'Imported workflow' }); await this.stabilizeTerminal(); const description = await (0, prompts_1.input)({ message: 'Description (optional):' }); await this.stabilizeTerminal(); const tags = await (0, prompts_1.input)({ message: 'Tags (comma-separated, optional):' }); const tagArray = tags ? tags.split(',').map((t) => t.trim()).filter(Boolean) : undefined; const saved = await this.storage.save(name, prompt, yamlContent, description, tagArray); this.log(chalk.green(`\n✅ Workflow imported: ${saved.name} (${saved.id})`)); this.log(`📁 Location: ~/.chromancer/workflows/${saved.id}.json`); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); this.error(`Failed to import workflow: ${errorMessage}`); } } async promptForFile() { await this.stabilizeTerminal(); const file = await (0, prompts_1.input)({ message: 'Path to YAML workflow file:', validate: async (value) => { try { await fs.access(value); return true; } catch { return 'File not found'; } } }); return file; } } exports.default = Workflows;