/** * @license * Copyright 2025 Vybestack LLC * SPDX-License-Identifier: Apache-2.0 */ import { type CommandModule } from 'yargs'; import { getExtensionAndConfig } from './utils.js'; import { getEnvContents, updateSetting, ExtensionSettingScope, } from '../../config/extensions/settingsIntegration.js'; import { loadSettings } from '../../config/settings.js'; import { exitCli } from '../utils.js'; interface SetArgs { name: string; setting: string; scope?: string; } interface ListArgs { name: string; scope?: string; } /** * Prompts for a setting value using readline. */ export async function promptForSetting( prompt: string, sensitive: boolean, ): Promise { if (sensitive && process.stdin.isTTY) { // Hide input for sensitive settings return new Promise((resolve) => { process.stdout.write(prompt); const stdin = process.stdin; const wasRaw = stdin.isRaw; stdin.setRawMode(true); stdin.resume(); let input = ''; const onData = (data: Buffer): void => { const char = data.toString('utf-8'); if (char === '\n' || char === '\r') { stdin.setRawMode(wasRaw); stdin.removeListener('data', onData); process.stdout.write('\n'); resolve(input); } else if (char === '\x7f' || char === '\b') { // Backspace input = input.slice(0, -1); } else if (char === '\x03') { // Ctrl+C stdin.setRawMode(wasRaw); stdin.removeListener('data', onData); process.stdout.write('\n'); resolve(''); } else { input += char; } }; stdin.on('data', onData); }); } const readline = await import('node:readline'); const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); return new Promise((resolve) => { rl.question(prompt, (answer) => { rl.close(); resolve(answer); }); }); } /** * Handler for 'extensions settings set' command. */ async function handleSet(args: SetArgs): Promise { const { extension, extensionConfig } = await getExtensionAndConfig(args.name); if (!extension || !extensionConfig) { return; } // Parse scope argument const scope = args.scope === 'workspace' ? ExtensionSettingScope.WORKSPACE : ExtensionSettingScope.USER; await updateSetting( extensionConfig.name, extension.path, args.setting, promptForSetting, scope, ); } /** * Handler for 'extensions settings list' command. */ async function handleList(args: ListArgs): Promise { const { extension, extensionConfig } = await getExtensionAndConfig(args.name); if (!extension || !extensionConfig) { return; } // Parse scope argument const scope = resolveScope(args.scope); const contents = await getEnvContents( extensionConfig.name, extension.path, scope, ); if (contents.length === 0) { globalThis.console.log(`Extension "${args.name}" has no settings.`); return; } const scopeLabel = scope != null ? ` (${scope} scope)` : ' (merged user + workspace)'; globalThis.console.log(`Settings for extension "${args.name}"${scopeLabel}:`); for (const { name, value } of contents) { globalThis.console.log(` ${name}: ${value}`); } } export const setCommand: CommandModule = { command: 'set ', describe: 'Sets a specific extension setting.', builder: (yargs) => yargs .positional('name', { describe: 'The name of the extension.', type: 'string', demandOption: true, }) .positional('setting', { describe: 'The name or environment variable of the setting to update.', type: 'string', demandOption: true, }) .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 handleSet({ name: argv['name'] as string, setting: argv['setting'] as string, scope: argv['scope'] as string | undefined, }); await exitCli(); }, }; export const listCommand: CommandModule = { command: 'list ', describe: 'Lists all settings for an extension.', builder: (yargs) => yargs .positional('name', { describe: 'The name of the extension.', type: 'string', demandOption: true, }) .option('scope', { describe: 'Setting scope: user, workspace, or omit to merge both', type: 'string', choices: ['user', 'workspace'], }), 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 handleList({ name: argv['name'] as string, scope: argv['scope'] as string | undefined, }); await exitCli(); }, }; export const settingsCommand: CommandModule = { command: 'settings ', describe: 'Manage extension settings.', builder: (yargs) => yargs .command(setCommand) .command(listCommand) .demandCommand(1, 'You need at least one command before continuing.') .version(false), handler: () => { // This handler is not called when a subcommand is provided. }, }; /** * Resolves the scope argument to an ExtensionSettingScope value. */ function resolveScope( scope: string | undefined, ): ExtensionSettingScope.WORKSPACE | ExtensionSettingScope.USER | undefined { if (scope === 'workspace') { return ExtensionSettingScope.WORKSPACE; } if (scope === 'user') { return ExtensionSettingScope.USER; } // undefined means merge both scopes return undefined; }