Skip to content

Commit 2c8cd74

Browse files
emilioaccclaude
andcommitted
feat: auto-update shell profile on login and read config file directly
- Add config.ts with centralized config handling: - getConnection() checks env var first, falls back to ~/.atxp/config - saveConnection() writes connection string to config file - getShellProfile() detects user's shell profile path - updateShellProfile() adds source line to shell profile - Update call-tool.ts to use getConnection() so CLI works even without ATXP_CONNECTION env var set (reads from config file) - Update login.ts to auto-update shell profile after saving config, eliminating the need for users to manually edit their shell profile This improves the login UX - users no longer need to manually update their ~/.zshrc or ~/.bashrc after running `npx atxp login`. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent c908827 commit 2c8cd74

File tree

3 files changed

+141
-22
lines changed

3 files changed

+141
-22
lines changed

packages/atxp/src/call-tool.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { atxpClient, ATXPAccount } from '@atxp/client';
22
import chalk from 'chalk';
3+
import { getConnection } from './config.js';
34

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

1516
if (!connection) {
1617
console.error(chalk.red('Not logged in.'));

packages/atxp/src/config.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import fs from 'fs';
2+
import path from 'path';
3+
import os from 'os';
4+
5+
export const CONFIG_DIR = path.join(os.homedir(), '.atxp');
6+
export const CONFIG_FILE = path.join(CONFIG_DIR, 'config');
7+
8+
/**
9+
* Get the ATXP connection string.
10+
* Checks env var first, falls back to reading from config file.
11+
*/
12+
export function getConnection(): string | null {
13+
// Check env var first
14+
if (process.env.ATXP_CONNECTION) {
15+
return process.env.ATXP_CONNECTION;
16+
}
17+
18+
// Fall back to reading from config file
19+
if (fs.existsSync(CONFIG_FILE)) {
20+
try {
21+
const content = fs.readFileSync(CONFIG_FILE, 'utf-8');
22+
// Parse: export ATXP_CONNECTION="..."
23+
const match = content.match(/export ATXP_CONNECTION="(.+)"/);
24+
if (match) {
25+
return match[1];
26+
}
27+
} catch {
28+
// Ignore read errors
29+
}
30+
}
31+
32+
return null;
33+
}
34+
35+
/**
36+
* Save the connection string to the config file.
37+
*/
38+
export function saveConnection(connectionString: string): void {
39+
if (!fs.existsSync(CONFIG_DIR)) {
40+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
41+
}
42+
43+
const configContent = `export ATXP_CONNECTION="${connectionString}"
44+
`;
45+
fs.writeFileSync(CONFIG_FILE, configContent, { mode: 0o600 });
46+
}
47+
48+
/**
49+
* Detect the user's shell profile path.
50+
* Returns null if shell cannot be detected or is unsupported.
51+
*/
52+
export function getShellProfile(): string | null {
53+
const shell = process.env.SHELL || '';
54+
const home = os.homedir();
55+
56+
if (shell.includes('zsh')) {
57+
return path.join(home, '.zshrc');
58+
}
59+
60+
if (shell.includes('bash')) {
61+
// On macOS, use .bash_profile; on Linux, use .bashrc
62+
if (process.platform === 'darwin') {
63+
return path.join(home, '.bash_profile');
64+
}
65+
return path.join(home, '.bashrc');
66+
}
67+
68+
// Fish uses different syntax, skip it
69+
// Other shells are unsupported
70+
return null;
71+
}
72+
73+
/**
74+
* Add "source ~/.atxp/config" to the user's shell profile if not already present.
75+
* Returns true if the profile was updated, false otherwise.
76+
*/
77+
export function updateShellProfile(): boolean {
78+
const profilePath = getShellProfile();
79+
if (!profilePath) {
80+
return false;
81+
}
82+
83+
const sourceLine = `source ${CONFIG_FILE}`;
84+
85+
try {
86+
let profileContent = '';
87+
88+
if (fs.existsSync(profilePath)) {
89+
profileContent = fs.readFileSync(profilePath, 'utf-8');
90+
// Check if already present
91+
if (profileContent.includes(sourceLine)) {
92+
return false;
93+
}
94+
}
95+
96+
// Add the source line with a comment
97+
const addition = `
98+
# ATXP CLI configuration
99+
if [ -f "${CONFIG_FILE}" ]; then
100+
${sourceLine}
101+
fi
102+
`;
103+
104+
fs.appendFileSync(profilePath, addition);
105+
return true;
106+
} catch {
107+
// Silently fail - CLI will still work via config file read
108+
return false;
109+
}
110+
}

packages/atxp/src/login.ts

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,16 @@
11
import http from 'http';
22
import fs from 'fs';
3-
import path from 'path';
4-
import os from 'os';
53
import chalk from 'chalk';
64
import open from 'open';
75
import qrcode from 'qrcode-terminal';
6+
import { saveConnection, updateShellProfile, CONFIG_FILE, getShellProfile } from './config.js';
87

98
interface LoginOptions {
109
force?: boolean;
1110
token?: string;
1211
qr?: boolean;
1312
}
1413

15-
const CONFIG_DIR = path.join(os.homedir(), '.atxp');
16-
const CONFIG_FILE = path.join(CONFIG_DIR, 'config');
17-
1814
export async function login(options: LoginOptions = {}): Promise<void> {
1915
// Check if already logged in
2016
const existingConnection = process.env.ATXP_CONNECTION;
@@ -43,16 +39,37 @@ export async function login(options: LoginOptions = {}): Promise<void> {
4339
connectionString = await loginWithBrowserOrQR();
4440
}
4541

46-
saveConnectionString(connectionString);
42+
saveConnection(connectionString);
43+
44+
// Try to auto-update shell profile
45+
const profileUpdated = updateShellProfile();
46+
const profilePath = getShellProfile();
4747

4848
console.log();
4949
console.log(chalk.green('Login successful!'));
50-
console.log();
51-
console.log('To use ATXP tools in this terminal, run:');
52-
console.log(chalk.cyan(` source ${CONFIG_FILE}`));
53-
console.log();
54-
console.log('Or add this to your shell profile (~/.bashrc, ~/.zshrc, etc.):');
55-
console.log(chalk.cyan(` source ${CONFIG_FILE}`));
50+
51+
if (profileUpdated && profilePath) {
52+
console.log();
53+
console.log(`Added ATXP to ${chalk.cyan(profilePath)}`);
54+
console.log('New terminal windows will automatically have access to ATXP tools.');
55+
console.log();
56+
console.log('To use ATXP in this terminal session, run:');
57+
console.log(chalk.cyan(` source ${CONFIG_FILE}`));
58+
} else if (profilePath) {
59+
// Profile exists but wasn't updated (already has source line)
60+
console.log();
61+
console.log("You're all set! New terminal windows will have access to ATXP tools.");
62+
console.log();
63+
console.log('To use ATXP in this terminal session, run:');
64+
console.log(chalk.cyan(` source ${CONFIG_FILE}`));
65+
} else {
66+
// Couldn't detect shell profile - provide manual instructions
67+
console.log();
68+
console.log('To use ATXP tools in this terminal, run:');
69+
console.log(chalk.cyan(` source ${CONFIG_FILE}`));
70+
console.log();
71+
console.log('To persist this, add the above line to your shell profile.');
72+
}
5673
} catch (error) {
5774
console.error(chalk.red('Login failed:'), error instanceof Error ? error.message : error);
5875
process.exit(1);
@@ -271,12 +288,3 @@ function getSuccessHTML(): string {
271288
`;
272289
}
273290

274-
function saveConnectionString(connectionString: string): void {
275-
if (!fs.existsSync(CONFIG_DIR)) {
276-
fs.mkdirSync(CONFIG_DIR, { recursive: true });
277-
}
278-
279-
const configContent = `export ATXP_CONNECTION="${connectionString}"
280-
`;
281-
fs.writeFileSync(CONFIG_FILE, configContent, { mode: 0o600 });
282-
}

0 commit comments

Comments
 (0)