@autifyhq/muon
Version:
Muon - AI-Powered Playwright Test Coding Agent with Advanced Test Fixing Capabilities
143 lines (142 loc) • 5.27 kB
JavaScript
import { resolve } from 'node:path';
import { Command } from 'commander';
import { render } from 'ink';
import React from 'react';
import { MuonAuth } from './auth.js';
import { isTrusted, saveTrustConfig } from './trust-config.js';
import { MuonApp } from './ui/app.js';
import { TrustConfirmation } from './ui/components.js';
const program = new Command();
program
.name('muon')
.description('Muon - AI-Powered Playwright Test Agent with Advanced Test Fixing Capabilities')
.version('0.0.8');
function validateApiKey(apiKey) {
if (!apiKey) {
return null;
}
if (!apiKey.startsWith('muon_live_') && !apiKey.startsWith('muon_test_')) {
throw new Error('Invalid API key format. API keys should start with "muon_live_" or "muon_test_"');
}
return apiKey;
}
async function getAuthCredentials(cliApiKey, serverUrl) {
// First, try to get valid access tokens (prioritize OAuth)
const auth = new MuonAuth(serverUrl);
const tokens = await auth.getValidTokens();
if (tokens) {
return { accessToken: tokens.accessToken, auth };
}
// Fallback to API key if no valid access tokens
const envApiKey = process.env['MUON_API_KEY'];
const apiKey = cliApiKey || envApiKey;
if (apiKey) {
try {
const validatedKey = validateApiKey(apiKey) || apiKey;
return { apiKey: validatedKey };
}
catch (error) {
console.error('❌ Invalid API key format:', error.message);
console.error('Get a valid API key from:', `${serverUrl}/keys`);
}
}
console.error('❌ Authentication is required to use Muon CLI');
console.error('');
console.error('Setup instructions:');
console.error('Option 1 - OAuth Authentication (Recommended):');
console.error(' muon login');
console.error('');
console.error('Option 2 - API Key:');
console.error(`1. Get your API key from: ${serverUrl}/keys`);
console.error('2. Set the environment variable:');
console.error(' export MUON_API_KEY=your_api_key_here');
console.error('');
console.error('Alternatively, you can pass it directly:');
console.error(' muon --api-key your_api_key_here');
console.error('');
process.exit(1);
}
async function showTrustConfirmation(projectPath) {
return new Promise((resolve) => {
const app = render(React.createElement(TrustConfirmation, {
projectPath,
onConfirm: () => {
// Save trust configuration when user confirms
const saved = saveTrustConfig(projectPath);
if (!saved) {
console.warn('Warning: Could not save trust configuration');
}
resolve(true);
},
onExit: () => {
resolve(false);
},
}));
app.waitUntilExit().then(() => {
// Cleanup will happen when the app exits
});
});
}
program
.command('start [projectPath]')
.description('Start Muon AI Test Agent with interactive terminal UI')
.option('-s, --server-url <url>', 'Server URL', 'https://muon.autify.com')
.option('-t, --agent-type <type>', 'Agent type (general)', 'general')
.option('-k, --api-key <key>', 'API key (overrides MUON_API_KEY environment variable)')
.option('--nlstep', 'Enable natural language step mode for complex/dynamic locators')
.action(async (projectPath = '.', options) => {
// Resolve the project path to absolute path to handle relative paths like "."
const resolvedProjectPath = resolve(projectPath);
// Authenticate first
const authCredentials = await getAuthCredentials(options.apiKey, options.serverUrl);
// Check if directory is already trusted
let trusted = isTrusted(resolvedProjectPath);
// If not trusted, show trust confirmation dialog after authentication
if (!trusted) {
trusted = await showTrustConfirmation(resolvedProjectPath);
if (!trusted) {
console.log('Trust declined. Exiting.');
process.exit(0);
}
}
const app = render(React.createElement(MuonApp, {
serverUrl: options.serverUrl,
agentType: options.agentType,
projectPath: resolvedProjectPath,
apiKey: authCredentials.apiKey,
accessToken: authCredentials.accessToken,
auth: authCredentials.auth,
nlstepMode: options.nlstep,
}));
try {
await app.waitUntilExit();
}
catch (error) {
console.error('Application error:', error);
process.exit(1);
}
});
program
.command('login')
.description('Authenticate with Muon using OAuth device flow')
.option('-s, --server-url <url>', 'Server URL', 'https://muon.autify.com')
.action(async (options) => {
const auth = new MuonAuth(options.serverUrl);
await auth.login();
});
program
.command('logout')
.description('Sign out from Muon')
.action(async () => {
const auth = new MuonAuth('');
await auth.logout();
});
program
.command('status')
.description('Check authentication status')
.action(async () => {
const auth = new MuonAuth('');
await auth.status();
});
program.parse();