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
90 changes: 73 additions & 17 deletions lib/installers/atl.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import chalk from 'chalk';
import inquirer from 'inquirer';
import { execSync } from 'child_process';
import { execFileSync, execSync } from 'child_process';
import fs from 'fs';
import os from 'os';
import path from 'path';
Expand Down Expand Up @@ -50,7 +50,7 @@ export const hasOAuthCredentials = () => {
if (binary) {
let output;
try {
output = execSync(`${binary} auth status`, {
output = execFileSync(binary, ['auth', 'status'], {
stdio: ['ignore', 'pipe', 'ignore'],
encoding: 'utf8',
});
Expand Down Expand Up @@ -79,18 +79,52 @@ export const hasOAuthCredentials = () => {
/**
* Check if atl-cli is authenticated (has valid token)
*/
export const isAtlAuthenticated = () => {
export const parseAtlAuthStatus = (output, hostname = '') => {
const statuses = JSON.parse(output);
if (!Array.isArray(statuses)) return false;
return hostname
? statuses.length === 1 && statuses[0]?.authenticated === true
: statuses.some((status) => status?.authenticated === true);
};

export const isAtlAuthenticated = (hostname = '') => {
const binary = findAtlBinary();
if (!binary) return false;

try {
const output = execSync(`${binary} auth status`, { stdio: 'pipe', encoding: 'utf8' });
return output.includes('Authenticated');
const args = ['auth', 'status', '--json'];
if (hostname) args.push('--hostname', hostname);
const output = execFileSync(binary, args, { stdio: 'pipe', encoding: 'utf8' });
return parseAtlAuthStatus(output, hostname);
} catch {
return false;
}
};

export const loginAtlTargets = (
binary,
targets,
{ login = execFileSync, isAuthenticated = isAtlAuthenticated } = {}
) => {
const failures = [];
for (const hostname of targets) {
try {
const args = ['auth', 'login'];
if (hostname) args.push('--hostname', hostname);
login(binary, args, { stdio: 'inherit' });
if (!isAuthenticated(hostname)) {
failures.push(hostname || 'default host');
}
} catch {
failures.push(hostname || 'default host');
}
}
return failures;
};

export const unauthenticatedAtlTargets = (hosts, isAuthenticated = isAtlAuthenticated) =>
hosts.filter((hostname) => !isAuthenticated(hostname));

/**
* Configure git and Go for private repo access
*/
Expand Down Expand Up @@ -191,7 +225,15 @@ const installAtl = async () => {
/**
* Configure Atlassian CLI
*/
export const configureAtlassianCli = async () => {
export const configureAtlassianCli = async ({ loginHosts = [] } = {}) => {
if (
!Array.isArray(loginHosts) ||
loginHosts.some((hostname) => typeof hostname !== 'string' || hostname.trim() === '')
) {
console.error(chalk.red('✗ loginHosts must contain non-empty hostnames or aliases'));
return false;
}

console.log(chalk.cyan('\n=== Atlassian CLI Configuration ===\n'));

const binary = findAtlBinary();
Expand Down Expand Up @@ -243,7 +285,9 @@ export const configureAtlassianCli = async () => {

// Check current state
const hasOAuth = hasOAuthCredentials();
const isAuthenticated = isAtlAuthenticated();
const missingHosts = unauthenticatedAtlTargets(loginHosts);
const isAuthenticated = loginHosts.length > 0 ? missingHosts.length === 0 : isAtlAuthenticated();
let loginTargets = loginHosts.length > 0 ? missingHosts : [''];

console.log(
chalk.blue('OAuth credentials:'),
Expand All @@ -255,7 +299,7 @@ export const configureAtlassianCli = async () => {
);

// If already fully set up, offer to reconfigure
if (isAuthenticated) {
if (hasOAuth && isAuthenticated) {
console.log(chalk.green('\n✓ Already authenticated with Atlassian'));

const { reconfigure } = await inquirer.prompt([
Expand All @@ -268,6 +312,9 @@ export const configureAtlassianCli = async () => {
]);

if (!reconfigure) return true;
// This branch is reachable only after every requested host passed the
// authentication check; explicit re-authentication intentionally refreshes all.
loginTargets = loginHosts.length > 0 ? loginHosts : [''];
}

// Step 1: OAuth setup (only if credentials don't exist)
Expand All @@ -287,7 +334,8 @@ export const configureAtlassianCli = async () => {

if (runSetup) {
try {
execSync(`${atlBinary} auth setup`, { stdio: 'inherit' });
execFileSync(atlBinary, ['auth', 'setup'], { stdio: 'inherit' });
loginTargets = loginHosts.length > 0 ? loginHosts : [''];
console.log(chalk.green('\n✓ OAuth setup completed'));
} catch (error) {
console.error(chalk.red(`\n✗ OAuth setup failed: ${error.message}`));
Expand All @@ -305,7 +353,13 @@ export const configureAtlassianCli = async () => {

// Step 2: Login (always run if not authenticated)
console.log(chalk.blue('\n--- Step 2: Login ---'));
console.log(chalk.gray('This will open a browser window for authentication.\n'));
console.log(
chalk.gray(
loginHosts.length > 0
? `This will authenticate: ${loginTargets.join(', ')}.\n`
: 'This will open a browser window for authentication.\n'
)
);

const { runLogin } = await inquirer.prompt([
{
Expand All @@ -322,13 +376,15 @@ export const configureAtlassianCli = async () => {
return false;
}

try {
execSync(`${atlBinary} auth login`, { stdio: 'inherit' });
console.log(chalk.green('\n✓ Atlassian CLI authenticated successfully'));
return true;
} catch (error) {
console.error(chalk.red(`\n✗ Authentication failed: ${error.message}`));
console.log(chalk.gray(`Try again with: ${atlBinary} auth login`));
const failures = loginAtlTargets(atlBinary, loginTargets);
if (failures.length > 0) {
console.error(chalk.red(`\n✗ Authentication failed for: ${failures.join(', ')}`));
for (const hostname of failures) {
const retry = hostname === 'default host' ? '' : ` --hostname ${hostname}`;
console.log(chalk.gray(`Try again with: ${atlBinary} auth login${retry}`));
}
return false;
}
console.log(chalk.green('\n✓ Atlassian CLI authenticated successfully'));
return true;
};
Loading