Skip to main content
Glama
question.ts6.64 kB
import {input, select, checkbox} from '@inquirer/prompts'; import chalk from 'chalk'; import boxen from 'boxen'; import type { Questionnaire, QuestionOpen, QuestionMultipleChoice, QuestionResponse, QuestionnaireResponse, } from '../types.js'; /** * Display utilities for questionnaire UI. */ export class QuestionnaireDisplay { /** * Display the questionnaire header. */ displayHeader(): void { console.log(); // Empty line for spacing const content = [ chalk.bold.blue('📋 Questionnaire'), '', chalk.dim('Please answer the following questions:'), '', chalk.dim('Press Ctrl+C to cancel at any time.'), ].join('\n'); console.log( boxen(content, { padding: 1, margin: 1, borderStyle: 'round', borderColor: 'blue', }), ); } /** * Display a success message. */ displaySuccess(message: string): void { console.log(chalk.green.bold('✅ ' + message)); } /** * Display an error message. */ displayError(message: string): void { console.log(chalk.red.bold('❌ ' + message)); } /** * Display a warning message. */ displayWarning(message: string): void { console.log(chalk.yellow.bold('⚠️ ' + message)); } /** * Display an info message. */ displayInfo(message: string): void { console.log(chalk.blue('ℹ️ ' + message)); } /** * Format the question message with optional description. */ formatQuestionMessage(question: QuestionOpen | QuestionMultipleChoice): string { let message = question.prompt; if (question.description) { message += `\n${chalk.dim(question.description)}`; } return message; } } /** * Question executor that handles different question types. */ export class QuestionExecutor { private readonly display: QuestionnaireDisplay; constructor() { this.display = new QuestionnaireDisplay(); } /** * Execute a questionnaire and return responses. */ async executeQuestionnaire(questionnaire: Questionnaire): Promise<QuestionnaireResponse> { // Display questionnaire header this.display.displayHeader(); const responses: QuestionResponse[] = []; let cancelled = false; let timedOut = false; try { // Process each question sequentially for (const question of questionnaire.questions) { // eslint-disable-next-line no-await-in-loop const response = await this.executeQuestion(question); responses.push(response); } } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; // Handle cancellation or timeout if (errorMessage.includes('cancelled') || errorMessage.includes('User cancelled')) { cancelled = true; this.display.displayWarning('Questionnaire cancelled by user.'); } else if (errorMessage.includes('timeout')) { timedOut = true; this.display.displayWarning('Questionnaire timed out.'); } else { // Re-throw unexpected errors throw error; } } return { responses, cancelled, timedOut, }; } /** * Execute a single question and return the response. */ async executeQuestion(question: QuestionOpen | QuestionMultipleChoice): Promise<QuestionResponse> { if (question.type === 'open') { return this.executeOpenQuestion(question); } return this.executeMultipleChoiceQuestion(question); } /** * Execute an open-ended question using text input. */ async executeOpenQuestion(question: QuestionOpen): Promise<QuestionResponse> { const answer = await input({ message: this.display.formatQuestionMessage(question), default: question.placeholder ?? '', validate(value: string) { // Apply validation rules if (question.minLength && value.length < question.minLength) { return `Answer must be at least ${question.minLength} characters long`; } if (question.maxLength && value.length > question.maxLength) { return `Answer must be no more than ${question.maxLength} characters long`; } return true; }, }); return { questionId: question.id, response: [answer], }; } /** * Execute a multiple choice question using select prompt. */ async executeMultipleChoiceQuestion(question: QuestionMultipleChoice): Promise<QuestionResponse> { const choices = question.options.map((option) => ({ name: option.description ? `${option.label} - ${chalk.dim(option.description)}` : option.label, value: option.id, checked: option.id === question.defaultOptionID, })); // Add "Other" option if allowOwnVariant is true (default) const allowOwnVariant = question.allowOwnVariant ?? true; const otherOptionId = '__other__'; if (allowOwnVariant) { choices.push({ name: chalk.italic('Other (specify your own answer)'), value: otherOptionId, checked: false, }); } if (question.allowMultiple) { // Use checkbox for multiple selections const answers = await checkbox({ message: this.display.formatQuestionMessage(question), choices, validate(choices) { const selected = choices.filter((choice) => choice.checked).map((choice) => choice.value); if (question.minSelections && selected.length < question.minSelections) { return `Please select at least ${question.minSelections} option(s)`; } if (question.maxSelections && selected.length > question.maxSelections) { return `Please select no more than ${question.maxSelections} option(s)`; } return true; }, }); // Handle "Other" selection for multiple choice let customText: string | undefined; const finalAnswers = answers.filter((answer) => answer !== otherOptionId); if (answers.includes(otherOptionId)) { customText = await input({ message: 'Please specify your own answer:', validate(value: string) { if (!value.trim()) { return 'Please provide a custom answer'; } return true; }, }); finalAnswers.push(customText); } return { questionId: question.id, response: finalAnswers, }; } // Use select for single selection const answer = await select({ message: this.display.formatQuestionMessage(question), choices, default: question.defaultOptionID, }); // Handle "Other" selection for single choice if (answer === otherOptionId) { const customText = await input({ message: 'Please specify your own answer:', validate(value: string) { if (!value.trim()) { return 'Please provide a custom answer'; } return true; }, }); return { questionId: question.id, response: [customText], customText, }; } return { questionId: question.id, response: [answer], }; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ver0-project/mcps'

If you have feedback or need assistance with the MCP directory API, please join our Discord server