import { addLog, ErrorLog, SpinnerLog, SuccessLog, removeLastLog } from '@mintlify/previewing'; import chalk from 'chalk'; import { Text } from 'ink'; import yaml from 'js-yaml'; import fs from 'node:fs/promises'; import path from 'node:path'; import type { Argv } from 'yargs'; import { CMD_EXEC_PATH, terminate } from '../helpers.js'; import { subdomainMiddleware } from '../middlewares/subdomainMiddleware.js'; import { trackEvent } from '../telemetry/track.js'; import { createWorkflow, deleteWorkflow, listWorkflows } from './client.js'; import { WORKFLOW_TYPES, type Workflow, type WorkflowBody, type WorkflowTrigger, type WorkflowType, } from './types.js'; type CreateArgs = { subdomain?: string; name?: string; prompt?: string; type?: string; cron?: string; pushRepo?: (string | number)[]; contextRepo?: (string | number)[]; automerge?: boolean; file?: string; format?: string; }; type OutputFormat = 'table' | 'json'; function resolveFormat(argv: { format?: string }): OutputFormat { return argv.format === 'json' ? 'json' : 'table'; } function output(format: OutputFormat, text: string) { if (format === 'table') { addLog({text}); } else { process.stdout.write(text + '\n'); } } function requireSubdomain(subdomain: string | undefined): string { if (!subdomain) { throw new Error( 'No subdomain set. Pass --subdomain, or run `mint config set subdomain `.' ); } return subdomain; } async function readWorkflowFile(filePath: string): Promise { const resolved = path.isAbsolute(filePath) ? filePath : path.join(CMD_EXEC_PATH, filePath); const contents = await fs.readFile(resolved, 'utf-8'); if (/\.ya?ml$/i.test(resolved)) return yaml.load(contents); return JSON.parse(contents); } function buildTriggerFromFlags(argv: CreateArgs): WorkflowTrigger { const repos = (argv.pushRepo ?? []).map(String).filter(Boolean); if (argv.cron && repos.length > 0) { throw new Error('Specify either --cron or --push-repo, not both.'); } if (argv.cron) return { cron: argv.cron }; if (repos.length > 0) return { push: repos.map((repo) => ({ repo })) }; throw new Error('A trigger is required. Pass --cron or --push-repo .'); } function buildBodyFromFlags(argv: CreateArgs): WorkflowBody { if (!argv.name) { throw new Error('--name is required when --file is not provided.'); } const type = argv.type; if (type !== undefined && !WORKFLOW_TYPES.includes(type as WorkflowType)) { throw new Error(`Invalid --type "${type}". Valid types: ${WORKFLOW_TYPES.join(', ')}`); } const body: WorkflowBody = { name: argv.name, on: buildTriggerFromFlags(argv), }; if (argv.prompt) body.prompt = argv.prompt; if (type) body.type = type as WorkflowType; if (argv.automerge !== undefined) body.automerge = argv.automerge; const contextRepos = (argv.contextRepo ?? []).map(String).filter(Boolean); if (contextRepos.length > 0) body.context = contextRepos.map((repo) => ({ repo })); return body; } function describeTrigger(trigger: WorkflowTrigger): string { if ('webhook' in trigger) return 'webhook'; if ('cron' in trigger) return `cron(${trigger.cron})`; if ('push' in trigger) return `push(${trigger.push.map((p) => p.repo).join(', ')})`; return `composio(${trigger.composio.toolkit}:${trigger.composio.triggerSlug})`; } function renderWorkflow(workflow: Workflow): string { const lines: string[] = []; lines.push(chalk.bold(`\nAutomation ${workflow.name}`)); lines.push(` ${chalk.dim('ID')} ${workflow._id}`); lines.push(` ${chalk.dim('Status')} ${workflow.status}`); if (workflow.type) lines.push(` ${chalk.dim('Type')} ${workflow.type}`); lines.push(` ${chalk.dim('Trigger')} ${describeTrigger(workflow.trigger)}`); if (workflow.automerge !== undefined) { lines.push(` ${chalk.dim('Automerge')} ${workflow.automerge}`); } return lines.join('\n'); } function renderList(workflows: Workflow[]): string { if (workflows.length === 0) return 'No automations found.'; const rows = workflows.map((w) => [ w._id, w.name, w.type ?? '—', describeTrigger(w.trigger), w.status, ]); const headers = ['ID', 'Name', 'Type', 'Trigger', 'Status']; const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => String(r[i]).length))); const fmt = (cells: string[]) => cells.map((c, i) => c.padEnd(widths[i]!)).join(' '); return [chalk.bold(fmt(headers)), ...rows.map(fmt)].join('\n'); } const withSubdomain = (yargs: Argv) => yargs.option('subdomain', { type: 'string' as const, description: 'Documentation subdomain (default: mint config set subdomain)', }); const withFormat = (yargs: Argv) => yargs.option('format', { type: 'string' as const, choices: ['table', 'json'] as const, description: 'Output format (table=pretty, json=raw)', }); export const workflowsBuilder = (yargs: Argv) => yargs .middleware(subdomainMiddleware) .command( 'create', 'Create a new automation', (yargs) => withFormat( withSubdomain(yargs) .option('name', { type: 'string', description: 'Automation name' }) .option('prompt', { type: 'string', description: 'Automation prompt' }) .option('type', { type: 'string', choices: WORKFLOW_TYPES, description: 'Automation type', }) .option('cron', { type: 'string', description: 'Cron expression for a scheduled trigger', }) .option('push-repo', { type: 'string', array: true, description: 'Repository (owner/repo) for a push trigger (repeatable)', }) .option('context-repo', { type: 'string', array: true, description: 'Additional context repository (repeatable)', }) .option('automerge', { type: 'boolean', description: 'Automatically merge PRs opened by this automation', }) .option('file', { type: 'string', description: 'Path to a JSON or YAML file with the full automation body', }) ) .example( 'mint automations create --name Translations --cron "0 6 * * *" --type translations', '' ) .example('mint automations create --file automation.yaml', ''), async (argv) => { const format = resolveFormat(argv); try { const subdomain = requireSubdomain(argv.subdomain); const body: WorkflowBody = argv.file ? ((await readWorkflowFile(argv.file)) as WorkflowBody) : buildBodyFromFlags(argv as CreateArgs); if (format === 'table') addLog(); const created = await createWorkflow(subdomain, body); if (format === 'table') removeLastLog(); void trackEvent('cli.automation.created', { subdomain, type: created.type, triggerKind: describeTrigger(created.trigger).split('(')[0], }); if (format === 'json') { output(format, JSON.stringify(created, null, 2)); } else { addLog(); output(format, renderWorkflow(created)); } await terminate(0); } catch (err) { if (format === 'table') removeLastLog(); const message = err instanceof Error ? err.message : 'unknown error'; if (format === 'json') { process.stderr.write(`Error: ${message}\n`); } else { addLog(); } await terminate(1); } } ) .command( 'list', 'List automations for the current deployment', (yargs) => withFormat(withSubdomain(yargs)), async (argv) => { const format = resolveFormat(argv); try { const subdomain = requireSubdomain(argv.subdomain); if (format === 'table') addLog(); const workflows = await listWorkflows(subdomain); if (format === 'table') removeLastLog(); if (format === 'json') { output(format, JSON.stringify(workflows, null, 2)); } else { output(format, renderList(workflows)); } await terminate(0); } catch (err) { if (format === 'table') removeLastLog(); const message = err instanceof Error ? err.message : 'unknown error'; if (format === 'json') { process.stderr.write(`Error: ${message}\n`); } else { addLog(); } await terminate(1); } } ) .command( 'delete ', 'Delete an automation by ID', (yargs) => withFormat(withSubdomain(yargs)).positional('id', { type: 'string', demandOption: true, description: 'Automation schema ID', }), async (argv) => { const format = resolveFormat(argv); try { const subdomain = requireSubdomain(argv.subdomain); if (format === 'table') addLog(); await deleteWorkflow(subdomain, argv.id); if (format === 'table') removeLastLog(); if (format === 'json') { output(format, JSON.stringify({ success: true, id: argv.id }, null, 2)); } else { addLog(); } await terminate(0); } catch (err) { if (format === 'table') removeLastLog(); const message = err instanceof Error ? err.message : 'unknown error'; if (format === 'json') { process.stderr.write(`Error: ${message}\n`); } else { addLog(); } await terminate(1); } } ) .demandCommand(1, 'specify a subcommand: create, list, or delete');