bktide
Version:
Command-line interface for Buildkite CI/CD workflows with rich shell completions (Fish, Bash, Zsh) and Alfred workflow integration for macOS power users
201 lines • 8.67 kB
JavaScript
import { BaseCommand } from './BaseCommand.js';
import { logger } from '../services/logger.js';
import { TokenSetupGuide } from '../services/TokenSetupGuide.js';
import prompts from 'prompts';
import { FormatterFactory, FormatterType } from '../formatters/FormatterFactory.js';
import { isRunningInAlfred } from '../utils/alfred.js';
import { Reporter } from '../ui/reporter.js';
export class ManageToken extends BaseCommand {
formatter;
reporter;
constructor(options) {
super(options);
this.formatter = FormatterFactory.getFormatter(FormatterType.TOKEN, options?.format);
this.reporter = new Reporter(options?.format || 'plain', options?.quiet, options?.tips);
}
static get requiresToken() {
return false;
}
async execute(options) {
try {
// Handle option priorities: if multiple options are provided,
// we'll process them in this priority: store > reset > check
if (options.store) {
const { success, errors } = await this.storeToken();
if (success) {
// Add next-steps hints after successful token storage (no redundant success message)
this.reporter.tips([
'Verify access with: bktide token --check',
'Explore your organizations: bktide orgs',
'List pipelines: bktide pipelines'
]);
return 0;
}
else {
const formattedErrors = this.formatter.formatAuthErrors('storing', errors);
logger.console(formattedErrors);
return 1;
}
}
else if (options.reset) {
await this.resetToken();
}
else if (options.check) {
const { errors } = await this.checkToken({ format: options.format });
if (errors.length > 0) {
const formattedErrors = this.formatter.formatAuthErrors('validating', errors);
logger.console(formattedErrors);
return 0;
}
}
else {
const { errors } = await this.checkOrStoreToken({ format: options.format });
if (errors.length > 0) {
const formattedErrors = this.formatter.formatAuthErrors('checking or storing', errors);
logger.console(formattedErrors);
return 0;
}
}
return 0; // Success
}
catch (error) {
const formattedError = this.formatter.formatError('executing', error);
logger.console(formattedError);
return 1; // Error
}
}
async storeToken() {
try {
let tokenToStore;
// If token is provided in options, use it
if (this.options.token) {
tokenToStore = this.options.token;
}
else {
const guide = new TokenSetupGuide();
const env = guide.detectEnvironment();
if (env === 'agent') {
const guidance = guide.getStoreGuidance();
logger.console(guidance);
return { success: false, errors: [new Error('Token setup must be done interactively by the user.')] };
}
if (isRunningInAlfred()) {
return { success: false, errors: [new Error('In Alfred, set token via Workflow Configuration.')] };
}
// Otherwise prompt the user
const response = await prompts({
type: 'password',
name: 'token',
message: 'Enter your Buildkite API token:',
validate: value => value.length > 0 ? true : 'Please enter a valid token'
});
// Check if user cancelled the prompt (Ctrl+C)
if (!response.token) {
return { success: false, errors: [new Error('Token storage cancelled')] };
}
tokenToStore = response.token;
}
// Ensure we have a valid token before proceeding
if (!tokenToStore) {
return { success: false, errors: [new Error('No token provided')] };
}
// Validate the token using the CredentialManager
const validationResult = await BaseCommand.credentialManager.validateToken(tokenToStore, {
showProgress: true // Show progress when validating during store
});
if (!validationResult.canListOrganizations) {
throw new Error('Token is invalid or does not have access to list organizations');
}
if (!validationResult.valid) {
const invalidOrgs = Object.entries(validationResult.organizations)
.filter(([_, status]) => !status.graphql || !status.builds || !status.organizations)
.map(([org, status]) => {
const invalidApis = [];
if (!status.graphql)
invalidApis.push('GraphQL');
if (!status.builds)
invalidApis.push('Builds');
if (!status.organizations)
invalidApis.push('Organizations');
return `${org} (${invalidApis.join(', ')})`;
});
throw new Error(`Token has limited access in some organizations: ${invalidOrgs.join(', ')}`);
}
// Store the token if it's valid
const success = await BaseCommand.credentialManager.saveToken(tokenToStore);
return { success, errors: [] };
}
catch (error) {
return { success: false, errors: [error] };
}
}
async resetToken() {
try {
const hadToken = await BaseCommand.credentialManager.hasToken();
let success = false;
if (hadToken) {
success = await BaseCommand.credentialManager.deleteToken();
}
const formattedResult = this.formatter.formatTokenResetResult(success, hadToken);
logger.console(formattedResult);
}
catch (error) {
const formattedError = this.formatter.formatError('resetting', error);
logger.console(formattedError);
}
}
async checkOrStoreToken(options) {
const { status, errors } = await this.checkToken({ ...options, suppressOutput: true });
if (!status.hasToken || !status.isValid) {
const { success, errors: storeErrors } = await this.storeToken();
if (success) {
return { stored: true, errors: [] };
}
else {
return { stored: false, errors: storeErrors };
}
}
return { stored: false, errors };
}
async checkToken(options) {
const errors = [];
// Get token using standard resolution: --token flag > env var > keychain
// This ensures `bktide token --check --token <token>` validates the provided token
// and `BUILDKITE_API_TOKEN=<token> bktide token --check` works as expected
const token = this.options.token
|| process.env.BUILDKITE_API_TOKEN
|| process.env.BK_TOKEN
|| await BaseCommand.credentialManager.getToken();
const hasToken = !!token;
let isValid = false;
let validation = {
valid: false,
canListOrganizations: false,
organizations: {}
};
if (hasToken && token) {
// Validate the token using the CredentialManager
try {
validation = await BaseCommand.credentialManager.validateToken(token, {
format: options?.format,
showProgress: true
});
isValid = validation.valid && validation.canListOrganizations;
}
catch (error) {
errors.push(error);
}
}
const tokenStatus = {
hasToken,
isValid,
validation
};
if (!options?.suppressOutput) {
const formattedResult = this.formatter.formatTokenStatus(tokenStatus);
logger.console(formattedResult);
}
return { status: tokenStatus, errors };
}
}
//# sourceMappingURL=ManageToken.js.map