basant307/AI_Governance_Project
048
1import type {2 ClaudeMarketplaceConfig,3 ExtensionConfig,4 ExtensionRequestOptions,5 SkillConfig,6 SubagentConfig,7} from '@qwen-code/qwen-code-core';8import type { ConfirmationRequest } from '../../ui/types.js';9import chalk from 'chalk';10import prompts from 'prompts';11import stripAnsi from 'strip-ansi';12import { t } from '../../i18n/index.js';13import { writeStdoutLine } from '../../utils/stdioHelpers.js';14 15/**16 * Requests consent from the user to perform an action, by reading a Y/n17 * character from stdin.18 *19 * This should not be called from interactive mode as it will break the CLI.20 *21 * @param consentDescription The description of the thing they will be consenting to.22 * @returns boolean, whether they consented or not.23 */24export async function requestConsentNonInteractive(25 consentDescription: string,26): Promise<boolean> {27 writeStdoutLine(consentDescription);28 const result = await promptForConsentNonInteractive(29 t('Do you want to continue? [Y/n]: '),30 );31 return result;32}33 34/**35 * Requests plugin selection from the user in non-interactive mode.36 * Displays an interactive list with arrow key navigation.37 *38 * This should not be called from interactive mode as it will break the CLI.39 *40 * @param marketplace The marketplace config containing available plugins.41 * @returns The name of the selected plugin.42 */43export async function requestChoicePluginNonInteractive(44 marketplace: ClaudeMarketplaceConfig,45): Promise<string> {46 const plugins = marketplace.plugins;47 48 if (plugins.length === 0) {49 throw new Error(t('No plugins available in this marketplace.'));50 }51 52 // Build choices for prompts select53 54 const choices = plugins.map((plugin) => ({55 title: chalk.green(chalk.bold(`[${plugin.name}]`)),56 value: plugin.name,57 }));58 59 const response = await prompts({60 type: 'select',61 name: 'plugin',62 message: t('Select a plugin to install from marketplace "{{name}}":', {63 name: marketplace.name,64 }),65 choices,66 initial: 0,67 });68 69 // Handle cancellation (Ctrl+C)70 if (response.plugin === undefined) {71 throw new Error(t('Plugin selection cancelled.'));72 }73 74 return response.plugin;75}76 77/**78 * Requests consent from the user to perform an action, in interactive mode.79 *80 * This should not be called from non-interactive mode as it will not work.81 *82 * @param consentDescription The description of the thing they will be consenting to.83 * @param addExtensionUpdateConfirmationRequest A function to actually add a prompt to the UI.84 * @returns boolean, whether they consented or not.85 */86export async function requestConsentInteractive(87 consentDescription: string,88 addExtensionUpdateConfirmationRequest: (value: ConfirmationRequest) => void,89): Promise<boolean> {90 return promptForConsentInteractive(91 consentDescription + '\n\n' + t('Do you want to continue?'),92 addExtensionUpdateConfirmationRequest,93 );94}95 96/**97 * Asks users a prompt and awaits for a y/n response on stdin.98 *99 * This should not be called from interactive mode as it will break the CLI.100 *101 * @param prompt A yes/no prompt to ask the user102 * @returns Whether or not the user answers 'y' (yes). Defaults to 'yes' on enter.103 */104async function promptForConsentNonInteractive(105 prompt: string,106): Promise<boolean> {107 const readline = await import('node:readline');108 const rl = readline.createInterface({109 input: process.stdin,110 output: process.stdout,111 });112 113 return new Promise((resolve) => {114 rl.question(prompt, (answer) => {115 rl.close();116 resolve(['y', ''].includes(answer.trim().toLowerCase()));117 });118 });119}120 121/**122 * Asks users an interactive yes/no prompt.123 *124 * This should not be called from non-interactive mode as it will break the CLI.125 *126 * @param prompt A markdown prompt to ask the user127 * @param addExtensionUpdateConfirmationRequest Function to update the UI state with the confirmation request.128 * @returns Whether or not the user answers yes.129 */130async function promptForConsentInteractive(131 prompt: string,132 addExtensionUpdateConfirmationRequest: (value: ConfirmationRequest) => void,133): Promise<boolean> {134 return new Promise<boolean>((resolve) => {135 addExtensionUpdateConfirmationRequest({136 prompt,137 onConfirm: (resolvedConfirmed) => {138 resolve(resolvedConfirmed);139 },140 });141 });142}143 144/**145 * Builds a consent string for installing an extension based on it's146 * extensionConfig.147 */148export function extensionConsentString(149 extensionConfig: ExtensionConfig,150 commands: string[] = [],151 skills: SkillConfig[] = [],152 subagents: SubagentConfig[] = [],153 originSource: string = 'QwenCode',154): string {155 const output: string[] = [];156 if (originSource !== 'QwenCode') {157 output.push(158 t(159 'You are installing an extension from {{originSource}}. Some features may not work perfectly with Qwen Code.',160 { originSource },161 ),162 );163 }164 const mcpServerEntries = Object.entries(extensionConfig.mcpServers || {});165 const displayLabel = extensionConfig.displayName ?? extensionConfig.name;166 output.push(167 t('Installing extension "{{name}}".', { name: displayLabel }),168 );169 if (170 typeof extensionConfig.description === 'string' &&171 extensionConfig.description172 ) {173 output.push(stripAnsi(extensionConfig.description));174 }175 output.push(176 t(177 '**Extensions may introduce unexpected behavior. Ensure you have investigated the extension source and trust the author.**',178 ),179 );180 181 if (mcpServerEntries.length) {182 output.push(t('This extension will run the following MCP servers:'));183 for (const [key, mcpServer] of mcpServerEntries) {184 const isLocal = !!mcpServer.command;185 const source =186 mcpServer.httpUrl ??187 `${mcpServer.command || ''}${mcpServer.args ? ' ' + mcpServer.args.join(' ') : ''}`;188 output.push(189 ` * ${key} (${isLocal ? t('local') : t('remote')}): ${source}`,190 );191 }192 }193 if (commands && commands.length > 0) {194 output.push(195 t('This extension will add the following commands: {{commands}}.', {196 commands: commands.join(', '),197 }),198 );199 }200 if (extensionConfig.contextFileName) {201 const fileName = Array.isArray(extensionConfig.contextFileName)202 ? extensionConfig.contextFileName.join(', ')203 : extensionConfig.contextFileName;204 output.push(205 t(206 'This extension will append info to your QWEN.md context using {{fileName}}',207 { fileName },208 ),209 );210 }211 if (skills.length > 0) {212 output.push(t('This extension will install the following skills:'));213 for (const skill of skills) {214 output.push(` * ${chalk.bold(skill.name)}: ${skill.description}`);215 }216 }217 if (subagents.length > 0) {218 output.push(t('This extension will install the following subagents:'));219 for (const subagent of subagents) {220 output.push(` * ${chalk.bold(subagent.name)}: ${subagent.description}`);221 }222 }223 return output.join('\n');224}225 226/**227 * Requests consent from the user to install an extension (extensionConfig), if228 * there is any difference between the consent string for `extensionConfig` and229 * `previousExtensionConfig`.230 *231 * Always requests consent if previousExtensionConfig is null.232 *233 * Throws if the user does not consent.234 */235export const requestConsentOrFail = async (236 requestConsent: (consent: string) => Promise<boolean>,237 options?: ExtensionRequestOptions,238) => {239 if (!options) return;240 const {241 extensionConfig,242 originSource = 'QwenCode',243 commands = [],244 skills = [],245 subagents = [],246 previousExtensionConfig,247 previousCommands = [],248 previousSkills = [],249 previousSubagents = [],250 } = options;251 const extensionConsent = extensionConsentString(252 extensionConfig,253 commands,254 skills,255 subagents,256 originSource,257 );258 if (previousExtensionConfig) {259 const previousExtensionConsent = extensionConsentString(260 previousExtensionConfig,261 previousCommands,262 previousSkills,263 previousSubagents,264 originSource,265 );266 if (previousExtensionConsent === extensionConsent) {267 return;268 }269 }270 if (!(await requestConsent(extensionConsent))) {271 throw new Error(272 t('Installation cancelled for "{{name}}".', {273 name: extensionConfig.name,274 }),275 );276 }277};278 