diff --git a/packages/atxp/src/login.ts b/packages/atxp/src/login.ts index 4cd5051..0515c23 100644 --- a/packages/atxp/src/login.ts +++ b/packages/atxp/src/login.ts @@ -1,8 +1,9 @@ +import http from 'http'; import fs from 'fs'; import path from 'path'; import os from 'os'; -import inquirer from 'inquirer'; import chalk from 'chalk'; +import open from 'open'; interface LoginOptions { force?: boolean; @@ -23,41 +24,118 @@ export async function login(options: LoginOptions = {}): Promise { console.log(chalk.bold('ATXP Login')); console.log(); - console.log(`Get your connection string from: ${chalk.cyan('https://accounts.atxp.ai')}`); - console.log(); - const { connectionString } = await inquirer.prompt([ - { - type: 'password', - name: 'connectionString', - message: 'Enter your connection string:', - mask: '*', - validate: (input: string) => { - if (!input || input.trim().length === 0) { - return 'Connection string is required'; - } - return true; - }, - }, - ]); - - // Create config directory if it doesn't exist + try { + const connectionString = await loginWithBrowser(); + saveConnectionString(connectionString); + + console.log(); + console.log(chalk.green('Login successful!')); + console.log(); + console.log('To use ATXP tools in this terminal, run:'); + console.log(chalk.cyan(` source ${CONFIG_FILE}`)); + console.log(); + console.log('Or add this to your shell profile (~/.bashrc, ~/.zshrc, etc.):'); + console.log(chalk.cyan(` source ${CONFIG_FILE}`)); + } catch (error) { + console.error(chalk.red('Login failed:'), error instanceof Error ? error.message : error); + process.exit(1); + } +} + +async function loginWithBrowser(): Promise { + return new Promise((resolve, reject) => { + const server = http.createServer((req, res) => { + const url = new URL(req.url!, `http://localhost`); + const connectionString = url.searchParams.get('connection_string'); + + if (connectionString) { + res.writeHead(200, { 'Content-Type': 'text/html' }); + res.end(` + + + + ATXP Login + + + +
+

Login Successful

+

You can close this tab and return to your terminal.

+
+ + + `); + server.close(); + resolve(decodeURIComponent(connectionString)); + } else { + res.writeHead(400, { 'Content-Type': 'text/plain' }); + res.end('Missing connection_string parameter'); + } + }); + + server.on('error', (err) => { + reject(new Error(`Failed to start local server: ${err.message}`)); + }); + + // Listen on random available port + server.listen(0, '127.0.0.1', () => { + const address = server.address(); + const port = typeof address === 'object' ? address?.port : null; + + if (!port) { + reject(new Error('Failed to start local server')); + return; + } + + const redirectUri = `http://localhost:${port}/callback`; + const loginUrl = `https://accounts.atxp.ai?cli_redirect=${encodeURIComponent(redirectUri)}`; + + console.log('Opening browser to complete login...'); + console.log(chalk.gray(`(${loginUrl})`)); + console.log(); + console.log('Waiting for authentication...'); + + open(loginUrl); + + // Timeout after 5 minutes + const timeout = setTimeout(() => { + server.close(); + reject(new Error('Login timed out. Please try again.')); + }, 5 * 60 * 1000); + + server.on('close', () => { + clearTimeout(timeout); + }); + }); + }); +} + +function saveConnectionString(connectionString: string): void { if (!fs.existsSync(CONFIG_DIR)) { fs.mkdirSync(CONFIG_DIR, { recursive: true }); } - // Write config file as shell export - const configContent = `export ATXP_CONNECTION="${connectionString.trim()}" + const configContent = `export ATXP_CONNECTION="${connectionString}" `; - fs.writeFileSync(CONFIG_FILE, configContent, { mode: 0o600 }); - - console.log(); - console.log(chalk.green('Login successful!')); - console.log(); - console.log('To use ATXP tools in this terminal, run:'); - console.log(chalk.cyan(` source ${CONFIG_FILE}`)); - console.log(); - console.log('Or add this to your shell profile (~/.bashrc, ~/.zshrc, etc.):'); - console.log(chalk.cyan(` source ${CONFIG_FILE}`)); }