UNPKG

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 9.52 kB
import { BaseCommand } from './BaseCommand.js'; import { getBuildFormatter } from '../formatters/index.js'; import Fuse from 'fuse.js'; import { logger } from '../services/logger.js'; import { Reporter } from '../ui/reporter.js'; import { Progress } from '../ui/progress.js'; export class ListBuilds extends BaseCommand { constructor(options) { super(options); } async execute(options) { await this.ensureInitialized(); const executeStartTime = process.hrtime.bigint(); if (options.debug) { logger.debug('Starting ViewerBuildsCommandHandler execution'); } try { const format = options.format || 'plain'; const reporter = new Reporter(format, options.quiet, options.tips); const viewerSpinner = Progress.spinner('Fetching viewer info...', { format }); // First, get the current user's information using GraphQL const viewerData = await this.client.getViewer(); viewerSpinner.stop(); if (!viewerData?.viewer?.user?.uuid) { logger.error('Failed to get current user UUID information'); return 1; } const userId = viewerData.viewer.user.uuid; const userName = viewerData.viewer.user.name || 'Current user'; const userEmail = viewerData.viewer.user.email; // Use the REST API to get builds by the current user const perPage = options.count || '10'; const page = options.page || '1'; // If organization is not specified, we need to fetch organizations first let orgs = []; if (!options.org) { // Try to fetch the user's organizations try { orgs = await this.client.getViewerOrganizationSlugs(); } catch (error) { logger.error(error, 'Failed to determine your organizations'); return 1; } } else { orgs = [options.org]; } // Initialize results array let allBuilds = []; let accessErrors = []; // Use progress bar for multiple orgs, spinner for single org const useProgressBar = orgs.length > 1 && format === 'plain'; const progress = useProgressBar ? Progress.bar({ total: orgs.length, label: 'Fetching builds from organizations', format: format }) : null; // Create a spinner for single-org scenario let fetchSpinner = !useProgressBar ? Progress.spinner(undefined, { format }) : null; for (let i = 0; i < orgs.length; i++) { const org = orgs[i]; try { if (progress) { progress.update(i, `Fetching builds from ${org}`); } else if (fetchSpinner) { fetchSpinner.update(`Fetching builds from ${org}…`, `Fetching builds from ${org}…`); } // First check if the user has access to this organization const hasAccess = await this.restClient.hasOrganizationAccess(org); if (!hasAccess) { accessErrors.push(`You don't have access to organization ${org}`); if (progress) { // Continue to next org with progress bar progress.update(i + 1, `No access to ${org}`); } else if (fetchSpinner) { fetchSpinner.fail(`No access to ${org}`); } continue; } const builds = await this.restClient.getBuilds(org, { creator: userId, pipeline: options.pipeline, branch: options.branch, state: options.state, per_page: perPage, page: page }); if (options.debug) { logger.debug(`Received ${builds.length} builds from org ${org}`); } allBuilds = allBuilds.concat(builds); if (!progress && fetchSpinner) { fetchSpinner.stop(); } } catch (error) { // Log unexpected errors but continue processing other orgs logger.error(error, `Error fetching builds for org ${org}`); if (!progress && fetchSpinner) { fetchSpinner.stop(); } } } // Complete the progress bar if (progress) { const successOrgs = orgs.length - accessErrors.length; progress.complete(`Retrieved ${allBuilds.length} builds from ${successOrgs}/${orgs.length} organizations`); } // Prepare formatter options const formatterOptions = { debug: options.debug, organizationsCount: orgs.length, orgSpecified: !!options.org, userName, userEmail, userId }; // Handle the case where we have no builds due to access issues if (allBuilds.length === 0 && accessErrors.length > 0) { formatterOptions.hasError = true; formatterOptions.errorType = 'access'; formatterOptions.accessErrors = accessErrors; if (options.org) { formatterOptions.errorMessage = `No builds found for ${userName} (${userEmail}) in organization ${options.org}. ${accessErrors[0]}`; } else { formatterOptions.errorMessage = `No builds found for ${userName} (${userEmail}). Try specifying an organization with --org to narrow your search.`; } const format = options.format || 'plain'; const formatter = getBuildFormatter(format); const output = formatter.formatBuilds(allBuilds, formatterOptions); logger.console(output); return 1; } // Limit to the requested number of builds allBuilds = allBuilds.slice(0, parseInt(perPage, 10)); // Apply fuzzy filter if specified if (options.filter) { if (options.debug) { logger.debug(`Applying fuzzy filter '${options.filter}' to ${allBuilds.length} builds`); } // Configure Fuse for fuzzy searching const fuse = new Fuse(allBuilds, { keys: ['pipeline.name', 'branch', 'message', 'creator.name', 'state'], threshold: 0.4, includeScore: true, shouldSort: true }); // Perform the fuzzy search const searchResults = fuse.search(options.filter); allBuilds = searchResults.map(result => result.item); if (options.debug) { logger.debug(`Filtered to ${allBuilds.length} builds matching '${options.filter}'`); } } const formatter = getBuildFormatter(format); const output = formatter.formatBuilds(allBuilds, formatterOptions); // Output data directly to stdout to ensure proper ordering if (format === 'plain') { process.stdout.write(output + '\n'); } else { logger.console(output); } // Add contextual next-steps hints AFTER showing the data if (allBuilds.length > 0) { const tips = []; const buildCount = parseInt(perPage, 10); if (allBuilds.length === buildCount) { tips.push(`Use --count ${buildCount * 2} to see more builds`); } // If filtering is not active, suggest filtering options if (!options.state && !options.branch && !options.pipeline) { tips.push('Filter by state: --state failed'); tips.push('Filter by branch: --branch main'); tips.push('Filter by pipeline: --pipeline <name>'); } // Display all tips at once if (tips.length > 0) { // Use individual style for consistency with current output tips.forEach(tip => reporter.tip(tip)); } } if (options.debug) { const executeDuration = Number(process.hrtime.bigint() - executeStartTime) / 1000000; logger.debug(`ViewerBuildsCommandHandler execution completed in ${executeDuration.toFixed(2)}ms`); } return 0; // Success } catch (error) { // Only unexpected errors should reach here // Let the base command handle the error display this.handleError(error, options.debug); return 1; // Error } } } //# sourceMappingURL=ListBuilds.js.map