/** * @license * Copyright 2025 Vybestack LLC * SPDX-License-Identifier: Apache-2.0 */ import { type CommandModule } from 'yargs'; import { getExtensionAndConfig } from './utils.js'; import { updateSetting, getScopedEnvContents, ExtensionSettingScope, loadExtensionSettingsFromManifest, } from '../../config/extensions/settingsIntegration.js'; import { loadUserExtensions, loadExtensionConfig, } from '../../config/extension.js'; import { loadSettings } from '../../config/settings.js'; import { exitCli } from '../utils.js'; import { promptForSetting } from './settings.js'; interface ConfigArgs { name?: string; setting?: string; scope?: string; } /** * Prompts for overwrite confirmation using readline. * Exported for testing. */ export async function confirmOverwrite(settingName: string): Promise { const readline = await import('node:readline'); const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); return new Promise((resolve) => { rl.question( `Setting "${settingName}" is already set. Overwrite? [y/N] `, (answer) => { rl.close(); resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes'); }, ); }); } /** * Configures a single setting for an extension. */ async function configureSetting( extensionName: string, extensionPath: string, settingKey: string, scope: ExtensionSettingScope, ): Promise { const success = await updateSetting( extensionName, extensionPath, settingKey, promptForSetting, scope, ); return success; } /** * Configures all settings for a single extension interactively. */ async function configureAllSettings( extensionName: string, extensionPath: string, scope: ExtensionSettingScope, ): Promise { const settings = loadExtensionSettingsFromManifest(extensionPath); if (settings.length === 0) { globalThis.console.log( `Extension "${extensionName}" has no settings to configure.`, ); return true; } globalThis.console.log( `Configuring settings for extension "${extensionName}"...`, ); const currentScopeValues = await getScopedEnvContents( extensionName, extensionPath, scope, ); // If configuring user scope, check for workspace values let workspaceScopeValues: Record = {}; if (scope === ExtensionSettingScope.USER) { workspaceScopeValues = await getScopedEnvContents( extensionName, extensionPath, ExtensionSettingScope.WORKSPACE, ); } let allSuccess = true; for (const setting of settings) { const currentValue = currentScopeValues[setting.envVar]; const workspaceValue = scope === ExtensionSettingScope.USER ? workspaceScopeValues[setting.envVar] : undefined; // Show advisory if configuring user scope but workspace has a value if (scope === ExtensionSettingScope.USER && workspaceValue) { globalThis.console.log( `Note: Setting "${setting.name}" has a workspace value. User scope value will take precedence.`, ); } // Check if value already set in current scope if (currentValue && currentValue !== '[not set]') { const shouldOverwrite = await confirmOverwrite(setting.name); if (!shouldOverwrite) { globalThis.console.log(`Skipping "${setting.name}".`); continue; } } const success = await updateSetting( extensionName, extensionPath, setting.envVar, promptForSetting, scope, ); if (!success) { allSuccess = false; globalThis.console.error( `Failed to configure setting "${setting.name}".`, ); } } return allSuccess; } async function configureNamedSetting( name: string, setting: string, scope: ExtensionSettingScope, ) { const { extension, extensionConfig } = await getExtensionAndConfig(name); if (!extension || !extensionConfig) { return; } await configureSetting(extensionConfig.name, extension.path, setting, scope); } async function configureNamedExtension( name: string, scope: ExtensionSettingScope, ) { const { extension, extensionConfig } = await getExtensionAndConfig(name); if (!extension || !extensionConfig) { return; } await configureAllSettings(extensionConfig.name, extension.path, scope); } async function configureInstalledExtension( extension: { name: string; path: string }, scope: ExtensionSettingScope, ): Promise { try { const extensionConfig = await loadExtensionConfig({ extensionDir: extension.path, workspaceDir: process.cwd(), }); if (!extensionConfig) { globalThis.console.error( `Failed to load configuration for extension "${extension.name}". Skipping.`, ); return false; } globalThis.console.log( `\nConfiguring extension "${extensionConfig.name}"...`, ); return await configureAllSettings( extensionConfig.name, extension.path, scope, ); } catch (error) { globalThis.console.error( `Error configuring extension "${extension.name}":`, error instanceof Error ? error.message : String(error), ); return false; } } async function configureInstalledExtensions(scope: ExtensionSettingScope) { const installedExtensions = loadUserExtensions(); if (installedExtensions.length === 0) { globalThis.console.log('No extensions installed.'); return; } globalThis.console.log( `Found ${installedExtensions.length} installed extension(s). Starting configuration...`, ); let overallSuccess = true; for (const extension of installedExtensions) { const success = await configureInstalledExtension(extension, scope); if (!success) { overallSuccess = false; } } if (overallSuccess) { globalThis.console.log('\nAll extensions configured successfully.'); return; } globalThis.console.error( '\nConfiguration completed with errors. Some extensions may not be fully configured.', ); process.exitCode = 1; } /** * Handler for 'extensions config' command. */ async function handleConfig(args: ConfigArgs): Promise { const scope = args.scope === 'workspace' ? ExtensionSettingScope.WORKSPACE : ExtensionSettingScope.USER; // Case 1: config - configure specific setting if (args.name && args.setting) { await configureNamedSetting(args.name, args.setting, scope); return; } // Case 2: config - configure all settings for one extension if (args.name) { await configureNamedExtension(args.name, scope); return; } // Case 3: config - configure all installed extensions await configureInstalledExtensions(scope); } export const configCommand: CommandModule = { command: 'config [name] [setting]', describe: 'Configure extension settings interactively.', builder: (yargs) => yargs .positional('name', { describe: 'The name of the extension (optional).', type: 'string', }) .positional('setting', { describe: 'The name or environment variable of a specific setting (optional).', type: 'string', }) .option('scope', { describe: 'Setting scope: user (default) or workspace', type: 'string', choices: ['user', 'workspace'], default: 'user', }), handler: async (argv) => { const settings = loadSettings(process.cwd()).merged; if (!(settings.experimental?.extensionConfig ?? false)) { globalThis.console.error( 'Extension configuration is currently disabled. Enable it by setting "experimental.extensionConfig" to true.', ); process.exitCode = 1; await exitCli(); return; } await handleConfig({ name: argv['name'] as string | undefined, setting: argv['setting'] as string | undefined, scope: argv['scope'] as string | undefined, }); await exitCli(); }, };