Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/atxp/src/call-tool.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { atxpClient, ATXPAccount } from '@atxp/client';
import chalk from 'chalk';
import { getConnection } from './config.js';

export interface ToolResult {
content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;
Expand All @@ -10,7 +11,7 @@ export async function callTool(
tool: string,
args: Record<string, unknown>
): Promise<string> {
const connection = process.env.ATXP_CONNECTION;
const connection = getConnection();

if (!connection) {
console.error(chalk.red('Not logged in.'));
Expand Down
110 changes: 110 additions & 0 deletions packages/atxp/src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import fs from 'fs';
import path from 'path';
import os from 'os';

export const CONFIG_DIR = path.join(os.homedir(), '.atxp');
export const CONFIG_FILE = path.join(CONFIG_DIR, 'config');

/**
* Get the ATXP connection string.
* Checks env var first, falls back to reading from config file.
*/
export function getConnection(): string | null {
// Check env var first
if (process.env.ATXP_CONNECTION) {
return process.env.ATXP_CONNECTION;
}

// Fall back to reading from config file
if (fs.existsSync(CONFIG_FILE)) {
try {
const content = fs.readFileSync(CONFIG_FILE, 'utf-8');
// Parse: export ATXP_CONNECTION="..."
const match = content.match(/export ATXP_CONNECTION="(.+)"/);
if (match) {
return match[1];
}
} catch {
// Ignore read errors
}
}

return null;
}

/**
* Save the connection string to the config file.
*/
export function saveConnection(connectionString: string): void {
if (!fs.existsSync(CONFIG_DIR)) {
fs.mkdirSync(CONFIG_DIR, { recursive: true });
}

const configContent = `export ATXP_CONNECTION="${connectionString}"
`;
fs.writeFileSync(CONFIG_FILE, configContent, { mode: 0o600 });
}

/**
* Detect the user's shell profile path.
* Returns null if shell cannot be detected or is unsupported.
*/
export function getShellProfile(): string | null {
const shell = process.env.SHELL || '';
const home = os.homedir();

if (shell.includes('zsh')) {
return path.join(home, '.zshrc');
}

if (shell.includes('bash')) {
// On macOS, use .bash_profile; on Linux, use .bashrc
if (process.platform === 'darwin') {
return path.join(home, '.bash_profile');
}
return path.join(home, '.bashrc');
}

// Fish uses different syntax, skip it
// Other shells are unsupported
return null;
}

/**
* Add "source ~/.atxp/config" to the user's shell profile if not already present.
* Returns true if the profile was updated, false otherwise.
*/
export function updateShellProfile(): boolean {
const profilePath = getShellProfile();
if (!profilePath) {
return false;
}

const sourceLine = `source ${CONFIG_FILE}`;

try {
let profileContent = '';

if (fs.existsSync(profilePath)) {
profileContent = fs.readFileSync(profilePath, 'utf-8');
// Check if already present
if (profileContent.includes(sourceLine)) {
return false;
}
}

// Add the source line with a comment
const addition = `
# ATXP CLI configuration
if [ -f "${CONFIG_FILE}" ]; then
${sourceLine}
fi
`;

fs.appendFileSync(profilePath, addition);
return true;
} catch {
// Silently fail - CLI will still work via config file read
return false;
}
}
50 changes: 29 additions & 21 deletions packages/atxp/src/login.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,16 @@
import http from 'http';
import fs from 'fs';
import path from 'path';
import os from 'os';
import chalk from 'chalk';
import open from 'open';
import qrcode from 'qrcode-terminal';
import { saveConnection, updateShellProfile, CONFIG_FILE, getShellProfile } from './config.js';

interface LoginOptions {
force?: boolean;
token?: string;
qr?: boolean;
}

const CONFIG_DIR = path.join(os.homedir(), '.atxp');
const CONFIG_FILE = path.join(CONFIG_DIR, 'config');

export async function login(options: LoginOptions = {}): Promise<void> {
// Check if already logged in
const existingConnection = process.env.ATXP_CONNECTION;
Expand Down Expand Up @@ -43,16 +39,37 @@
connectionString = await loginWithBrowserOrQR();
}

saveConnectionString(connectionString);
saveConnection(connectionString);

// Try to auto-update shell profile
const profileUpdated = updateShellProfile();
const profilePath = getShellProfile();

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}`));

if (profileUpdated && profilePath) {
console.log();
console.log(`Added ATXP to ${chalk.cyan(profilePath)}`);
console.log('New terminal windows will automatically have access to ATXP tools.');
console.log();
console.log('To use ATXP in this terminal session, run:');
console.log(chalk.cyan(` source ${CONFIG_FILE}`));
} else if (profilePath) {
// Profile exists but wasn't updated (already has source line)
console.log();
console.log("You're all set! New terminal windows will have access to ATXP tools.");
console.log();
console.log('To use ATXP in this terminal session, run:');
console.log(chalk.cyan(` source ${CONFIG_FILE}`));
} else {
// Couldn't detect shell profile - provide manual instructions
console.log();
console.log('To use ATXP tools in this terminal, run:');
console.log(chalk.cyan(` source ${CONFIG_FILE}`));
console.log();
console.log('To persist this, add the above line to your shell profile.');
}
} catch (error) {
console.error(chalk.red('Login failed:'), error instanceof Error ? error.message : error);
process.exit(1);
Expand Down Expand Up @@ -142,7 +159,7 @@
console.log('Waiting for authentication...');
console.log(chalk.gray('(If browser did not open, press Ctrl+C and run: npx atxp login --qr)'));

} catch (openError) {

Check warning on line 162 in packages/atxp/src/login.ts

View workflow job for this annotation

GitHub Actions / test

'openError' is defined but never used
// Browser open failed, fall back to QR
console.log();
console.log(chalk.yellow('Could not open browser. Switching to QR code...'));
Expand Down Expand Up @@ -271,12 +288,3 @@
`;
}

function saveConnectionString(connectionString: string): void {
if (!fs.existsSync(CONFIG_DIR)) {
fs.mkdirSync(CONFIG_DIR, { recursive: true });
}

const configContent = `export ATXP_CONNECTION="${connectionString}"
`;
fs.writeFileSync(CONFIG_FILE, configContent, { mode: 0o600 });
}