veas
Version:
Veas CLI - Command-line interface for Veas platform
529 lines • 22.8 kB
JavaScript
import { hostname as getHostname } from 'node:os';
import * as prompts from '@clack/prompts';
import { createClient } from '@supabase/supabase-js';
import chalk from 'chalk';
import { config as loadEnv } from 'dotenv';
import ora from 'ora';
import { AuthManager } from '../auth/auth-manager.js';
loadEnv({ path: '.env.local' });
loadEnv();
export async function listDestinations(options) {
const spinner = ora('Fetching destinations...').start();
try {
const authManager = AuthManager.getInstance();
const session = await authManager.getSession();
if (!session) {
spinner.fail('Not authenticated. Please run "veas auth login" first.');
process.exit(1);
}
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL || process.env.SUPABASE_URL || 'http://127.0.0.1:54321';
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY ||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY ||
process.env.SUPABASE_ANON_KEY;
if (!supabaseUrl || !supabaseKey) {
spinner.fail('Supabase configuration not found.');
process.exit(1);
}
const supabase = createClient(supabaseUrl, supabaseKey);
let organizationId = options.organizationId;
if (!organizationId) {
console.log(chalk.gray(`Fetching organizations for user: ${session.user.id}`));
const { data: memberships, error: memberError } = await supabase
.schema('team_management')
.from('organization_members')
.select(`
organization_id,
user_id,
role
`)
.eq('user_id', session.user.id);
let organizationsData = [];
if (memberships && memberships.length > 0) {
const orgIds = memberships.map(m => m.organization_id);
const { data: orgs } = await supabase
.schema('team_management')
.from('organizations')
.select('id, name, slug')
.in('id', orgIds);
organizationsData = orgs || [];
}
const membershipsWithOrgs = memberships?.map(m => ({
...m,
organization: organizationsData.find((o) => o.id === m.organization_id),
})) || [];
if (memberError) {
console.error(chalk.red('Error fetching organizations:'), memberError);
spinner.fail(`Failed to fetch organizations: ${memberError.message}`);
process.exit(1);
}
console.log(chalk.gray(`Found ${membershipsWithOrgs?.length || 0} organization memberships`));
if (!membershipsWithOrgs || membershipsWithOrgs.length === 0) {
spinner.fail('User must belong to at least one organization');
process.exit(1);
}
if (membershipsWithOrgs.length === 1) {
organizationId = membershipsWithOrgs[0]?.organization_id;
}
else {
spinner.stop();
const orgChoice = await prompts.select({
message: 'Select organization:',
options: membershipsWithOrgs.map(m => ({
value: m.organization_id,
label: m.organization?.name || m.organization_id,
hint: m.organization?.slug,
})),
});
if (prompts.isCancel(orgChoice)) {
console.log(chalk.yellow('Operation cancelled'));
process.exit(0);
}
organizationId = orgChoice;
spinner.start('Fetching destinations...');
}
}
if (!organizationId) {
spinner.fail('Organization ID required');
process.exit(1);
}
const { data: destinations, error } = await supabase
.schema('agents')
.from('agent_destinations')
.select('*')
.eq('organization_id', organizationId)
.order('created_at', { ascending: false });
if (error) {
throw error;
}
spinner.succeed('Destinations fetched');
if (options.json) {
console.log(JSON.stringify(destinations, null, 2));
return;
}
if (!destinations || destinations.length === 0) {
console.log(chalk.yellow('No destinations found'));
return;
}
console.log(chalk.bold('\nAgent Destinations:'));
console.log(chalk.gray('─'.repeat(80)));
for (const dest of destinations) {
const status = getStatusColor(dest.status);
console.log(`\n${chalk.bold(dest.name)} ${status(dest.status)}`);
console.log(` ID: ${chalk.gray(dest.id)}`);
console.log(` Hostname: ${dest.hostname}`);
console.log(` Max Tasks: ${dest.max_concurrent_tasks}`);
console.log(` Last Heartbeat: ${dest.last_heartbeat_at ? new Date(dest.last_heartbeat_at).toLocaleString() : 'Never'}`);
console.log(` Total Executions: ${dest.total_executions || 0}`);
console.log(` Success Rate: ${dest.total_executions > 0 ? Math.round((dest.successful_executions / dest.total_executions) * 100) : 0}%`);
if (dest.tags?.length > 0) {
console.log(` Tags: ${dest.tags.join(', ')}`);
}
}
}
catch (error) {
spinner.fail(`Failed to list destinations: ${error instanceof Error ? error.message : String(error)}`);
process.exit(1);
}
}
export async function registerDestination(options) {
const spinner = ora('Registering destination...').start();
try {
const authManager = AuthManager.getInstance();
const session = await authManager.getSession();
if (!session) {
spinner.fail('Not authenticated. Please run "veas auth login" first.');
process.exit(1);
}
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL || process.env.SUPABASE_URL || 'http://127.0.0.1:54321';
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY ||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY ||
process.env.SUPABASE_ANON_KEY;
if (!supabaseUrl || !supabaseKey) {
spinner.fail('Supabase configuration not found.');
process.exit(1);
}
const supabase = createClient(supabaseUrl, supabaseKey);
console.log(chalk.gray(`Checking organizations for user: ${session.user.id}`));
const { data: memberships, error: memberError } = await supabase
.schema('team_management')
.from('organization_members')
.select(`
organization_id,
user_id,
role
`)
.eq('user_id', session.user.id);
if (memberError) {
console.error(chalk.red('Error fetching organizations:'), memberError);
spinner.fail(`Failed to fetch organizations: ${memberError.message}`);
process.exit(1);
}
let organizationsData = [];
if (memberships && memberships.length > 0) {
const orgIds = memberships.map(m => m.organization_id);
const { data: orgs } = await supabase
.schema('team_management')
.from('organizations')
.select('id, name, slug')
.in('id', orgIds);
organizationsData = orgs || [];
}
const membershipsWithOrgs = memberships?.map(m => ({
...m,
organization: organizationsData.find((o) => o.id === m.organization_id),
})) || [];
console.log(chalk.gray(`Found ${membershipsWithOrgs?.length || 0} organization memberships`));
if (!membershipsWithOrgs || membershipsWithOrgs.length === 0) {
spinner.fail('User must belong to at least one organization');
process.exit(1);
}
spinner.stop();
let selectedOrgId;
if (options.organizationId) {
const membership = membershipsWithOrgs.find(m => m.organization_id === options.organizationId);
if (!membership) {
spinner.fail(`You don't have access to organization ${options.organizationId}`);
process.exit(1);
}
selectedOrgId = options.organizationId;
const orgName = membership.organization?.name || membership.organization_id;
console.log(chalk.gray(`Using organization: ${orgName}`));
}
else if (membershipsWithOrgs.length === 1) {
const membership = membershipsWithOrgs[0];
if (!membership) {
spinner.fail('No organization membership found');
process.exit(1);
}
selectedOrgId = membership.organization_id;
const orgName = membership.organization?.name || membership.organization_id;
console.log(chalk.gray(`Using organization: ${orgName}`));
}
else {
const orgChoice = await prompts.select({
message: 'Select organization:',
options: membershipsWithOrgs.map(m => ({
value: m.organization_id,
label: m.organization?.name || m.organization_id,
hint: m.organization?.slug,
})),
});
if (prompts.isCancel(orgChoice)) {
console.log(chalk.yellow('Registration cancelled'));
process.exit(0);
}
selectedOrgId = orgChoice;
}
const name = await prompts.text({
message: 'Destination name:',
placeholder: 'my-agent-server',
validate: value => {
if (!value)
return 'Name is required';
return undefined;
},
});
if (prompts.isCancel(name)) {
console.log(chalk.yellow('Registration cancelled'));
process.exit(0);
}
const hostname = await prompts.text({
message: 'Hostname:',
placeholder: 'agent-server-1.example.com',
initialValue: getHostname(),
});
if (prompts.isCancel(hostname)) {
console.log(chalk.yellow('Registration cancelled'));
process.exit(0);
}
const maxTasks = await prompts.text({
message: 'Max concurrent tasks:',
placeholder: '3',
initialValue: '3',
validate: value => {
const num = parseInt(value, 10);
if (Number.isNaN(num) || num < 1)
return 'Must be a positive number';
return undefined;
},
});
if (prompts.isCancel(maxTasks)) {
console.log(chalk.yellow('Registration cancelled'));
process.exit(0);
}
spinner.start('Registering destination...');
const apiKey = generateApiKey();
const apiKeyHash = await hashApiKey(apiKey);
const { data: destination, error } = await supabase
.schema('agents')
.from('agent_destinations')
.insert({
organization_id: selectedOrgId,
owner_id: session.user.id,
name,
hostname,
max_concurrent_tasks: parseInt(maxTasks, 10),
api_key_hash: apiKeyHash,
status: 'offline',
is_active: true,
capabilities: {},
supported_tools: [],
allowed_task_types: ['workflow', 'single', 'batch'],
})
.select()
.single();
if (error) {
throw error;
}
spinner.succeed('Destination registered successfully!');
console.log(chalk.green('\n✅ Destination Details:'));
console.log(` ID: ${destination.id}`);
console.log(` Name: ${destination.name}`);
console.log(` Hostname: ${destination.hostname}`);
console.log(`\n${chalk.yellow('⚠️ API Key (save this securely):')}`);
console.log(chalk.bold(` ${apiKey}`));
console.log(chalk.gray('\nThis API key will not be shown again.'));
console.log(chalk.gray('Use it when starting agents on this destination.'));
}
catch (error) {
spinner.fail(`Failed to register destination: ${error instanceof Error ? error.message : String(error)}`);
process.exit(1);
}
}
export async function deleteDestination(destinationId, options) {
const spinner = ora('Checking destination...').start();
try {
const authManager = AuthManager.getInstance();
const session = await authManager.getSession();
if (!session) {
spinner.fail('Not authenticated. Please run "veas auth login" first.');
process.exit(1);
}
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL || process.env.SUPABASE_URL || 'http://127.0.0.1:54321';
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY ||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY ||
process.env.SUPABASE_ANON_KEY;
if (!supabaseUrl || !supabaseKey) {
spinner.fail('Supabase configuration not found.');
process.exit(1);
}
const supabase = createClient(supabaseUrl, supabaseKey);
const { data: destination, error: checkError } = await supabase
.schema('agents')
.from('agent_destinations')
.select('id, name, status')
.eq('id', destinationId)
.single();
if (checkError || !destination) {
spinner.fail('Destination not found');
process.exit(1);
}
spinner.stop();
if (!options.force) {
const confirm = await prompts.confirm({
message: `Delete destination "${destination.name}"?`,
});
if (!confirm || prompts.isCancel(confirm)) {
console.log(chalk.yellow('Deletion cancelled'));
process.exit(0);
}
}
spinner.start('Deleting destination...');
const { error: deleteError } = await supabase
.schema('agents')
.from('agent_destinations')
.delete()
.eq('id', destinationId);
if (deleteError) {
throw deleteError;
}
spinner.succeed(`Destination "${destination.name}" deleted successfully`);
}
catch (error) {
spinner.fail(`Failed to delete destination: ${error instanceof Error ? error.message : String(error)}`);
process.exit(1);
}
}
export async function watchDestination(destinationId, options) {
const spinner = ora('Connecting to destination...').start();
try {
const authManager = AuthManager.getInstance();
const session = await authManager.getSession();
if (!session) {
spinner.fail('Not authenticated. Please run "veas auth login" first.');
process.exit(1);
}
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL || process.env.SUPABASE_URL || 'http://127.0.0.1:54321';
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY ||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY ||
process.env.SUPABASE_ANON_KEY;
if (!supabaseUrl || !supabaseKey) {
spinner.fail('Supabase configuration not found.');
process.exit(1);
}
const supabase = createClient(supabaseUrl, supabaseKey);
const { data: destination, error: destError } = await supabase
.schema('agents')
.from('agent_destinations')
.select('*')
.eq('id', destinationId)
.single();
if (destError || !destination) {
spinner.fail('Destination not found');
process.exit(1);
}
spinner.succeed(`Connected to destination: ${destination.name}`);
console.log(chalk.gray('Starting schedule monitor and watching for executions...'));
if (options.verbose) {
console.log(chalk.yellow('Verbose mode: ENABLED'));
console.log(chalk.gray('[VERBOSE] Destination ID:', destinationId));
console.log(chalk.gray('[VERBOSE] Organization ID:', destination.organization_id));
console.log(chalk.gray('[VERBOSE] Supabase URL:', supabaseUrl));
}
console.log(chalk.gray('Press Ctrl+C to stop\n'));
const { ScheduleMonitor } = await import('../services/schedule-monitor.js');
const monitor = new ScheduleMonitor(supabase, destinationId, destination.organization_id, options.verbose);
await monitor.start();
process.on('SIGINT', async () => {
console.log(chalk.yellow('\n\nStopping monitor...'));
await monitor.stop();
process.exit(0);
});
await new Promise(() => { });
}
catch (error) {
spinner.fail(`Failed to watch destination: ${error instanceof Error ? error.message : String(error)}`);
process.exit(1);
}
}
function getStatusColor(status) {
switch (status) {
case 'online':
return chalk.green;
case 'offline':
return chalk.gray;
case 'busy':
return chalk.yellow;
case 'maintenance':
return chalk.blue;
case 'error':
return chalk.red;
case 'completed':
return chalk.green;
case 'failed':
return chalk.red;
case 'running':
return chalk.cyan;
case 'pending':
return chalk.yellow;
default:
return chalk.white;
}
}
export async function listDestinationSchedules(destinationId, options) {
const spinner = ora('Fetching schedules...').start();
try {
const authManager = AuthManager.getInstance();
const session = await authManager.getSession();
if (!session) {
spinner.fail('Not authenticated. Please run "veas auth login" first.');
process.exit(1);
}
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL || process.env.SUPABASE_URL || 'http://127.0.0.1:54321';
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY ||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY ||
process.env.SUPABASE_ANON_KEY;
if (!supabaseUrl || !supabaseKey) {
spinner.fail('Supabase configuration not found.');
process.exit(1);
}
const supabase = createClient(supabaseUrl, supabaseKey);
const { data: schedules, error } = await supabase
.schema('agents')
.from('schedules')
.select(`
*,
tasks!inner(
id,
name,
description
)
`)
.eq('destination_id', destinationId)
.order('priority', { ascending: false })
.order('start_time', { ascending: true });
if (error) {
throw error;
}
spinner.stop();
if (!schedules || schedules.length === 0) {
console.log(chalk.yellow('No schedules found for this destination'));
return;
}
if (options.json) {
console.log(JSON.stringify(schedules, null, 2));
return;
}
console.log(chalk.bold('\nDestination Schedules:'));
console.log(chalk.gray('─'.repeat(80)));
for (const schedule of schedules) {
const task = schedule.tasks;
console.log(chalk.bold(`\n📅 ${schedule.title || task.name}`));
if (schedule.description) {
console.log(chalk.gray(` ${schedule.description}`));
}
console.log(chalk.cyan(` Task: ${task.name}`));
console.log(chalk.gray(` Type: ${schedule.schedule_type}`));
if (schedule.schedule_type === 'calendar') {
console.log(chalk.gray(` Start: ${new Date(schedule.start_time).toLocaleString()}`));
if (schedule.end_time) {
console.log(chalk.gray(` End: ${new Date(schedule.end_time).toLocaleString()}`));
}
else if (schedule.duration_minutes) {
console.log(chalk.gray(` Duration: ${schedule.duration_minutes} minutes`));
}
if (schedule.recurrence_rule) {
console.log(chalk.gray(` Recurrence: ${schedule.recurrence_rule}`));
}
if (schedule.all_day) {
console.log(chalk.gray(` All Day: Yes`));
}
}
else if (schedule.schedule_type === 'cron') {
console.log(chalk.gray(` Cron: ${schedule.cron_expression}`));
}
else if (schedule.schedule_type === 'interval') {
console.log(chalk.gray(` Interval: ${schedule.interval_seconds} seconds`));
}
console.log(chalk.gray(` Priority: ${schedule.priority === 0 ? 'Low' : schedule.priority === 1 ? 'Normal' : schedule.priority === 2 ? 'High' : 'Critical'}`));
console.log(chalk.gray(` Timezone: ${schedule.timezone}`));
console.log(chalk.gray(` Enabled: ${schedule.is_enabled ? 'Yes' : 'No'}`));
if (schedule.next_run_at) {
console.log(chalk.yellow(` Next Run: ${new Date(schedule.next_run_at).toLocaleString()}`));
}
if (schedule.last_run_at) {
console.log(chalk.gray(` Last Run: ${new Date(schedule.last_run_at).toLocaleString()}`));
}
console.log(chalk.gray(` Run Count: ${schedule.run_count}`));
}
console.log(chalk.gray(`\n${'─'.repeat(80)}`));
console.log(chalk.green(`Total: ${schedules.length} schedule(s)`));
}
catch (error) {
spinner.fail(`Failed to fetch schedules: ${error instanceof Error ? error.message : String(error)}`);
process.exit(1);
}
}
function generateApiKey() {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let key = 'dest_';
for (let i = 0; i < 32; i++) {
key += chars.charAt(Math.floor(Math.random() * chars.length));
}
return key;
}
async function hashApiKey(apiKey) {
const crypto = await import('node:crypto');
return crypto.createHash('sha256').update(apiKey).digest('hex');
}
//# sourceMappingURL=destination.js.map