mnee-cli
Version:
A CLI tool for interacting with MNEE USD
192 lines (191 loc) • 7.55 kB
JavaScript
import { promises as fs } from 'fs';
import path from 'path';
import os from 'os';
import http from 'http';
import { URL } from 'url';
import open from 'open';
const CONFIG_DIR = path.join(os.homedir(), '.mnee');
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
export async function ensureConfigDir() {
try {
await fs.mkdir(CONFIG_DIR, { recursive: true });
}
catch (error) {
// Directory already exists
}
}
export async function loadConfig() {
try {
await ensureConfigDir();
const data = await fs.readFile(CONFIG_FILE, 'utf-8');
return JSON.parse(data);
}
catch (error) {
return {};
}
}
export async function saveConfig(config) {
await ensureConfigDir();
await fs.writeFile(CONFIG_FILE, JSON.stringify(config, null, 2));
}
export async function clearConfig() {
try {
await fs.unlink(CONFIG_FILE);
}
catch (error) {
// File doesn't exist
}
}
export async function startAuthFlow(apiUrl) {
return new Promise((resolve, reject) => {
let server = null;
// Find an available port
const tryPort = async (port) => {
return new Promise((resolve, reject) => {
const testServer = http.createServer();
testServer.listen(port, '127.0.0.1', () => {
testServer.close(() => resolve(port));
});
testServer.on('error', () => {
// Port is in use, try next one
resolve(tryPort(port + 1));
});
});
};
(async () => {
try {
const port = await tryPort(8900);
const redirectUri = `http://localhost:${port}/callback`;
// Start local server to receive callback
server = http.createServer((req, res) => {
const url = new URL(req.url || '', `http://localhost:${port}`);
if (url.pathname === '/callback') {
const error = url.searchParams.get('error');
if (error) {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(`
<html>
<body style="font-family: system-ui, sans-serif; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0;">
<div style="text-align: center;">
<h1 style="color: #dc2626;">Authentication Failed</h1>
<p>${error === 'access_denied' ? 'Access was denied' : 'An error occurred during authentication'}</p>
<p style="color: #666;">You can close this window and return to your terminal.</p>
</div>
</body>
</html>
`);
server?.close();
reject(new Error(error));
return;
}
// Success page
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(`
<html>
<body style="font-family: system-ui, sans-serif; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0;">
<div style="text-align: center;">
<h1 style="color: #16a34a;">Authentication Successful!</h1>
<p>You can close this window and return to your terminal.</p>
</div>
</body>
</html>
`);
}
});
server.listen(port, '127.0.0.1');
// Initialize auth session
const initResponse = await fetch(`${apiUrl}/cli/auth/init`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ redirectUri }),
});
if (!initResponse.ok) {
const errorText = await initResponse.text();
console.error('API Error:', initResponse.status, errorText);
throw new Error(`Failed to initialize authentication session: ${initResponse.status}`);
}
const initData = await initResponse.json();
console.log(`\nOpening browser for authentication...`);
console.log(`If the browser doesn't open, visit: ${initData.authUrl}\n`);
// Open browser
await open(initData.authUrl);
// Poll for completion
let attempts = 0;
const maxAttempts = 120; // 10 minutes (5 second intervals)
while (attempts < maxAttempts) {
await new Promise(resolve => setTimeout(resolve, 5000)); // Wait 5 seconds
const statusResponse = await fetch(`${apiUrl}/cli/auth/status/${initData.state}`);
if (!statusResponse.ok) {
attempts++;
continue;
}
const status = await statusResponse.json();
if (status.status === 'completed') {
server?.close();
resolve(status);
return;
}
else if (status.status === 'error') {
throw new Error(status.message || 'Authentication failed');
}
attempts++;
}
throw new Error('Authentication timeout');
}
catch (error) {
server?.close();
reject(error);
}
})();
});
}
export async function validateToken(apiUrl, token) {
try {
const response = await fetch(`${apiUrl}/cli/auth/profile`, {
headers: {
'Authorization': `Bearer ${token}`,
},
});
return response.ok;
}
catch (error) {
return false;
}
}
export async function getProfile(apiUrl, token) {
const response = await fetch(`${apiUrl}/cli/auth/profile`, {
headers: {
'Authorization': `Bearer ${token}`,
},
});
if (!response.ok) {
throw new Error('Failed to get profile');
}
return response.json();
}
export async function logout(apiUrl, token) {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 second timeout
const response = await fetch(`${apiUrl}/cli/auth/logout`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
},
signal: controller.signal,
});
clearTimeout(timeoutId);
if (!response.ok && response.status !== 404) {
console.warn('⚠️ Logout API returned an error, but local session cleared.');
}
}
catch (error) {
if (error.name === 'AbortError') {
console.warn('⚠️ Logout request timed out, but local session cleared.');
}
else if (error.code === 'ECONNREFUSED') {
console.warn('⚠️ Could not connect to server, but local session cleared.');
}
// Still proceed with local logout even if API call fails
}
}