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
6 changes: 3 additions & 3 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { updateCommand } from './commands/update.js';
import { whoamiCommand } from './commands/whoami.js';
import { version } from './index.js';
import { AuthError, getMe } from './services/auth.service.js';
import { loadConfig } from './utils/config.js';
import { loadAuth } from './utils/config.js';
import { checkForUpdates } from './utils/version.js';

const displayBanner = (): void => {
Expand All @@ -38,8 +38,8 @@ const displayVersionInfo = async (): Promise<void> => {

// Display login status
try {
const config = await loadConfig();
if (config.auth?.token) {
const auth = await loadAuth();
if (auth.token) {
const user = await getMe();
const displayName = user.name || user.email;
const aliasDisplay = user.verifiedAlias
Expand Down
44 changes: 20 additions & 24 deletions src/commands/login.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ const mockRequestDeviceCode =
const mockPollForToken = authService.pollForToken as jest.MockedFunction<
typeof authService.pollForToken
>;
const mockLoadConfig = configUtils.loadConfig as jest.MockedFunction<
typeof configUtils.loadConfig
const mockLoadAuth = configUtils.loadAuth as jest.MockedFunction<
typeof configUtils.loadAuth
>;
const mockSaveConfig = configUtils.saveConfig as jest.MockedFunction<
typeof configUtils.saveConfig
const mockSaveAuth = configUtils.saveAuth as jest.MockedFunction<
typeof configUtils.saveAuth
>;
const mockGetRegistryUrl = configUtils.getRegistryUrl as jest.MockedFunction<
typeof configUtils.getRegistryUrl
Expand Down Expand Up @@ -56,9 +56,7 @@ describe('loginCommand', () => {
});

it('shows message when already logged in', async () => {
mockLoadConfig.mockResolvedValue({
auth: { token: 'existing-token' },
});
mockLoadAuth.mockResolvedValue({ token: 'existing-token' });

await loginCommand();

Expand All @@ -71,7 +69,7 @@ describe('loginCommand', () => {
});

it('completes login flow successfully', async () => {
mockLoadConfig.mockResolvedValue({});
mockLoadAuth.mockResolvedValue({});
mockRequestDeviceCode.mockResolvedValue({
device_code: 'device123',
user_code: 'ABCD-1234',
Expand All @@ -84,18 +82,16 @@ describe('loginCommand', () => {
token_type: 'Bearer',
user: { id: '1', email: 'test@example.com', name: 'Test User' },
});
mockSaveConfig.mockResolvedValue(undefined);
mockSaveAuth.mockResolvedValue(undefined);

await loginCommand();

expect(mockRequestDeviceCode).toHaveBeenCalled();
expect(mockPollForToken).toHaveBeenCalledWith('device123', 5, 900);
expect(mockSaveConfig).toHaveBeenCalledWith({
auth: {
token: 'new-token',
user: { id: '1', email: 'test@example.com', name: 'Test User' },
expiresAt: undefined,
},
expect(mockSaveAuth).toHaveBeenCalledWith({
token: 'new-token',
user: { id: '1', email: 'test@example.com', name: 'Test User' },
expiresAt: undefined,
});
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining('Logged in as'),
Expand All @@ -104,7 +100,7 @@ describe('loginCommand', () => {
});

it('handles login errors', async () => {
mockLoadConfig.mockResolvedValue({});
mockLoadAuth.mockResolvedValue({});
mockRequestDeviceCode.mockRejectedValue(
new AuthError('Failed', 'request_failed')
);
Expand All @@ -118,7 +114,7 @@ describe('loginCommand', () => {
});

it('handles expired token error with hint', async () => {
mockLoadConfig.mockResolvedValue({});
mockLoadAuth.mockResolvedValue({});
mockRequestDeviceCode.mockResolvedValue({
device_code: 'device123',
user_code: 'ABCD-1234',
Expand All @@ -144,7 +140,7 @@ describe('loginCommand', () => {
});

it('handles non-AuthError errors', async () => {
mockLoadConfig.mockResolvedValue({});
mockLoadAuth.mockResolvedValue({});
mockRequestDeviceCode.mockRejectedValue(new Error('Network error'));

await expect(loginCommand()).rejects.toThrow('process.exit called');
Expand All @@ -156,7 +152,7 @@ describe('loginCommand', () => {
});

it('shows email when user has no name', async () => {
mockLoadConfig.mockResolvedValue({});
mockLoadAuth.mockResolvedValue({});
mockRequestDeviceCode.mockResolvedValue({
device_code: 'device123',
user_code: 'ABCD-1234',
Expand All @@ -169,7 +165,7 @@ describe('loginCommand', () => {
token_type: 'Bearer',
user: { id: '1', email: 'test@example.com' },
});
mockSaveConfig.mockResolvedValue(undefined);
mockSaveAuth.mockResolvedValue(undefined);

await loginCommand();

Expand All @@ -180,7 +176,7 @@ describe('loginCommand', () => {
});

it('shows generic success when no user info', async () => {
mockLoadConfig.mockResolvedValue({});
mockLoadAuth.mockResolvedValue({});
mockRequestDeviceCode.mockResolvedValue({
device_code: 'device123',
user_code: 'ABCD-1234',
Expand All @@ -192,7 +188,7 @@ describe('loginCommand', () => {
access_token: 'new-token',
token_type: 'Bearer',
});
mockSaveConfig.mockResolvedValue(undefined);
mockSaveAuth.mockResolvedValue(undefined);

await loginCommand();

Expand All @@ -207,7 +203,7 @@ describe('loginCommand', () => {
default: jest.fn().mockRejectedValue(new Error('Cannot open browser')),
}));

mockLoadConfig.mockResolvedValue({});
mockLoadAuth.mockResolvedValue({});
mockRequestDeviceCode.mockResolvedValue({
device_code: 'device123',
user_code: 'ABCD-1234',
Expand All @@ -220,7 +216,7 @@ describe('loginCommand', () => {
token_type: 'Bearer',
user: { id: '1', email: 'test@example.com', name: 'Test User' },
});
mockSaveConfig.mockResolvedValue(undefined);
mockSaveAuth.mockResolvedValue(undefined);

await loginCommand();

Expand Down
26 changes: 10 additions & 16 deletions src/commands/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@ import {
pollForToken,
requestDeviceCode,
} from '../services/auth.service.js';
import { getRegistryUrl, loadConfig, saveConfig } from '../utils/config.js';
import { getRegistryUrl, loadAuth, saveAuth } from '../utils/config.js';

/**
* Login command - authenticate via device authorization flow
*/
export const loginCommand = async (): Promise<void> => {
try {
// Check if already logged in
const config = await loadConfig();
if (config.auth?.token) {
const auth = await loadAuth();
if (auth.token) {
console.log(
chalk.yellow('⚠️ Already logged in.'),
'Run',
Expand Down Expand Up @@ -65,19 +65,13 @@ export const loginCommand = async (): Promise<void> => {
deviceCode.expires_in
);

// Reload config to get any changes made during auth (e.g., deviceId)
const currentConfig = await loadConfig();

// Save token to config
await saveConfig({
...currentConfig,
auth: {
token: tokenResponse.access_token,
user: tokenResponse.user,
expiresAt: tokenResponse.expires_in
? new Date(Date.now() + tokenResponse.expires_in * 1000).toISOString()
: undefined,
},
// Save token to auth.json (config.json with deviceId/registry is untouched)
await saveAuth({
token: tokenResponse.access_token,
user: tokenResponse.user,
expiresAt: tokenResponse.expires_in
? new Date(Date.now() + tokenResponse.expires_in * 1000).toISOString()
: undefined,
});

console.log();
Expand Down
32 changes: 14 additions & 18 deletions src/commands/logout.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ jest.mock('../utils/config.js');
const mockLogout = authService.logout as jest.MockedFunction<
typeof authService.logout
>;
const mockLoadConfig = configUtils.loadConfig as jest.MockedFunction<
typeof configUtils.loadConfig
const mockLoadAuth = configUtils.loadAuth as jest.MockedFunction<
typeof configUtils.loadAuth
>;
const mockClearConfig = configUtils.clearConfig as jest.MockedFunction<
typeof configUtils.clearConfig
const mockClearAuth = configUtils.clearAuth as jest.MockedFunction<
typeof configUtils.clearAuth
>;

describe('logoutCommand', () => {
Expand All @@ -29,46 +29,42 @@ describe('logoutCommand', () => {
});

it('shows message when not logged in', async () => {
mockLoadConfig.mockResolvedValue({});
mockLoadAuth.mockResolvedValue({});

await logoutCommand();

expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining('Not logged in')
);
expect(mockClearConfig).not.toHaveBeenCalled();
expect(mockClearAuth).not.toHaveBeenCalled();
});

it('clears credentials and shows success', async () => {
mockLoadConfig.mockResolvedValue({
auth: {
token: 'test-token',
user: { id: '1', email: 'test@example.com', name: 'Test User' },
},
mockLoadAuth.mockResolvedValue({
token: 'test-token',
user: { id: '1', email: 'test@example.com', name: 'Test User' },
});
mockLogout.mockResolvedValue(undefined);
mockClearConfig.mockResolvedValue(undefined);
mockClearAuth.mockResolvedValue(undefined);

await logoutCommand();

expect(mockLogout).toHaveBeenCalled();
expect(mockClearConfig).toHaveBeenCalled();
expect(mockClearAuth).toHaveBeenCalled();
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining('Logged out from'),
expect.stringContaining('Test User')
);
});

it('clears credentials even when server logout fails', async () => {
mockLoadConfig.mockResolvedValue({
auth: { token: 'test-token' },
});
mockLoadAuth.mockResolvedValue({ token: 'test-token' });
mockLogout.mockRejectedValue(new Error('Network error'));
mockClearConfig.mockResolvedValue(undefined);
mockClearAuth.mockResolvedValue(undefined);

await logoutCommand();

expect(mockClearConfig).toHaveBeenCalled();
expect(mockClearAuth).toHaveBeenCalled();
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining('Logged out locally')
);
Expand Down
14 changes: 7 additions & 7 deletions src/commands/logout.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
import chalk from 'chalk';
import { logout as logoutApi } from '../services/auth.service.js';
import { clearConfig, loadConfig } from '../utils/config.js';
import { clearAuth, loadAuth } from '../utils/config.js';

/**
* Logout command - clear stored credentials
*/
export const logoutCommand = async (): Promise<void> => {
try {
// Check if logged in
const config = await loadConfig();
if (!config.auth?.token) {
const auth = await loadAuth();
if (!auth.token) {
console.log(chalk.yellow('Not logged in.'));
return;
}

// Get user info before clearing
const userName = config.auth.user?.name || config.auth.user?.email;
const userName = auth.user?.name || auth.user?.email;

// Attempt server-side logout (optional, ignore errors)
console.log(chalk.gray('Logging out...'));
await logoutApi();

// Clear local credentials
await clearConfig();
// Clear auth.json only — config.json (deviceId, registry) is preserved
await clearAuth();

console.log();
if (userName) {
Expand All @@ -32,7 +32,7 @@ export const logoutCommand = async (): Promise<void> => {
}
} catch {
// Even if server logout fails, clear local credentials
await clearConfig();
await clearAuth();
console.log(chalk.green('✅ Logged out locally.'));
console.log(
chalk.gray(
Expand Down
Loading
Loading