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');