diff --git a/src/commands/chat.ts b/src/commands/chat.ts index f62aa3b..eadeadc 100644 --- a/src/commands/chat.ts +++ b/src/commands/chat.ts @@ -4,10 +4,12 @@ import chalk from 'chalk'; import { ChatCompletionsService, ApiError } from '../generated/index'; import { ConfigManager } from '../config/manager'; import { configureOpenAPI } from '../utils/openapi-config'; +import { OptionResolver } from '../utils/option-resolver'; interface ChatOptions { replicaName?: string; message?: string; + silent?: boolean; nonInteractive?: boolean; verbose?: boolean; veryVerbose?: boolean; @@ -15,10 +17,8 @@ interface ChatOptions { export async function chatCommand(folderPath?: string, options: ChatOptions = {}): Promise { const targetPath = folderPath || '.'; - + try { - // Load configurations following standard priority pattern - const { projectConfig } = await ConfigManager.getMergedConfig(targetPath); const effectiveConfig = await ConfigManager.getEffectiveConfig(targetPath); if (!effectiveConfig.apiKey) { @@ -31,36 +31,23 @@ export async function chatCommand(folderPath?: string, options: ChatOptions = {} } // Configure the OpenAPI client - configureOpenAPI({ - ...effectiveConfig, - verbose: options.verbose, - veryVerbose: options.veryVerbose + configureOpenAPI({ + ...effectiveConfig, + verbose: options.verbose, + veryVerbose: options.veryVerbose }); - // Get replica name following standard priority pattern - let { replicaName, message } = options; - - if (!replicaName) { - if (options.nonInteractive) { - // Try project config, then fail - replicaName = projectConfig.replicaName; - if (!replicaName) { - console.error(chalk.red('❌ Missing --replica-name parameter in non-interactive mode')); - console.error(chalk.red(' Either provide --replica-name or set replicaName in sensay.config.json')); - process.exit(1); - } - } else { - // Interactive prompt with default from config - const { replica } = await inquirer.prompt({ - type: 'input', - name: 'replica', - message: 'Replica name:', - default: projectConfig.replicaName, - validate: (input: string) => input.trim().length > 0 || 'Replica name is required' - }); - replicaName = replica; - } - } + // Resolve options using OptionResolver + const resolver = new OptionResolver(targetPath, { silent: options.silent, nonInteractive: options.nonInteractive }); + + const replicaName = await resolver.resolve({ + key: 'replicaName', + cliValue: options.replicaName, + promptMessage: 'Replica name:', + isRequired: true, + }); + + const message = options.message; // Find replica UUID by name const { ReplicasService } = await import('../generated/index'); @@ -80,10 +67,10 @@ export async function chatCommand(folderPath?: string, options: ChatOptions = {} process.exit(1); } - if (options.nonInteractive) { - // Non-interactive mode: single message + if (options.nonInteractive || options.silent) { + // Non-interactive/silent mode: single message if (!message) { - console.error(chalk.red('❌ Missing --message parameter in non-interactive mode')); + console.error(chalk.red('❌ Missing --message parameter in non-interactive/silent mode')); process.exit(1); } @@ -188,7 +175,11 @@ export function setupChatCommand(program: Command): void { .option('-m, --message ', 'single message for non-interactive mode') .action((folderPath, options) => { const globalOptions = program.opts(); - return chatCommand(folderPath, { ...options, nonInteractive: globalOptions.nonInteractive }); + return chatCommand(folderPath, { + ...options, + silent: globalOptions.silent, + nonInteractive: globalOptions.nonInteractive, + }); }); // Configure help in wget style for this command diff --git a/src/commands/simple-organization-setup.ts b/src/commands/simple-organization-setup.ts index 7a9e5a8..a0f943f 100644 --- a/src/commands/simple-organization-setup.ts +++ b/src/commands/simple-organization-setup.ts @@ -3,21 +3,23 @@ import inquirer from 'inquirer'; import chalk from 'chalk'; import * as path from 'path'; import * as fs from 'fs-extra'; -import { - ApiError, - UsersService, +import { + ApiError, + UsersService, ReplicasService } from '../generated/index'; import { ConfigManager } from '../config/manager'; import { FileProcessor } from '../utils/files'; import { ProgressManager } from '../utils/progress'; import { configureOpenAPI } from '../utils/openapi-config'; +import { OptionResolver } from '../utils/option-resolver'; interface SetupOptions { folderPath?: string; userName?: string; userEmail?: string; replicaName?: string; + silent?: boolean; nonInteractive?: boolean; force?: boolean; verbose?: boolean; @@ -114,54 +116,26 @@ export async function simpleOrganizationSetupCommand(folderPath?: string, option veryVerbose: options.veryVerbose }); - // Get or prompt for configuration values - let { userName, userEmail } = options; + // Resolve configuration values using OptionResolver + const resolver = new OptionResolver(targetPath, { silent: options.silent, nonInteractive: options.nonInteractive }); - if (!userName || !userEmail) { - const currentConfig = { - userName: userName || projectConfig.userName, - userEmail: userEmail || projectConfig.userEmail, - }; - - if (options.nonInteractive) { - // In non-interactive mode, use existing config or fail - if (!currentConfig.userName || !currentConfig.userEmail) { - console.error(chalk.red('❌ Missing required configuration. In non-interactive mode, you must either:')); - console.error(chalk.red(' 1. Provide command line options: --user-name, --user-email')); - console.error(chalk.red(' 2. Or have these values in your project config file (sensay.config.json)')); - process.exit(1); - } - userName = currentConfig.userName; - userEmail = currentConfig.userEmail; - } else { - const questions = [ - { - type: 'input', - name: 'userName', - message: 'User name:', - default: currentConfig.userName, - when: !currentConfig.userName, - validate: (input: string) => input.trim().length > 0 || 'User name is required' - }, - { - type: 'input', - name: 'userEmail', - message: 'User email:', - default: currentConfig.userEmail, - when: !currentConfig.userEmail, - validate: (input: string) => { - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - return emailRegex.test(input) || 'Please enter a valid email address'; - } - } - ]; + const userName = await resolver.resolve({ + key: 'userName', + cliValue: options.userName, + promptMessage: 'User name:', + isRequired: true, + }); - const answers = await inquirer.prompt(questions); - - userName = userName || currentConfig.userName || answers.userName; - userEmail = userEmail || currentConfig.userEmail || answers.userEmail; + const userEmail = await resolver.resolve({ + key: 'userEmail', + cliValue: options.userEmail, + promptMessage: 'User email:', + isRequired: true, + validator: (input: string) => { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(input) || 'Please enter a valid email address'; } - } + }); // Save project configuration (without replicaName since we have multiple) await ConfigManager.saveProjectConfig({ @@ -426,8 +400,9 @@ export function setupSimpleOrganizationSetupCommand(program: Command): void { .option('-f, --force', 'skip confirmation before deleting existing training data') .action((folderPath, options) => { const globalOptions = program.opts(); - return simpleOrganizationSetupCommand(folderPath, { - ...options, + return simpleOrganizationSetupCommand(folderPath, { + ...options, + silent: globalOptions.silent, nonInteractive: globalOptions.nonInteractive, verbose: globalOptions.verbose, veryVerbose: globalOptions.veryVerbose diff --git a/src/index.ts b/src/index.ts index 9e0a796..cc2c0c0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -28,6 +28,7 @@ program .option('-v, --verbose', 'Enable verbose output (shows API requests METHOD, URL, and response status)') .option('-vv, --very-verbose', 'Enable very verbose output (shows full request/response including headers and body)') .option('--no-color', 'Disable colored output') + .option('-s, --silent', 'Skip interactive prompts, use config defaults (error if required value missing)') .option('--non-interactive', 'Disable interactive mode (for scripts/CI)'); // Setup commands diff --git a/src/utils/option-resolver.ts b/src/utils/option-resolver.ts new file mode 100644 index 0000000..6fd506e --- /dev/null +++ b/src/utils/option-resolver.ts @@ -0,0 +1,85 @@ +import inquirer from 'inquirer'; +import { ConfigManager, ProjectConfig } from '../config/manager'; +import { SensayConfig } from '../types/api'; + +export interface ResolveParams { + key: string; + cliValue?: string; + envVar?: string; + promptMessage: string; + isRequired?: boolean; + validator?: (input: string) => boolean | string; + defaultValue?: string; +} + +export class OptionResolver { + private folderPath: string; + private silent: boolean; + private projectConfig: ProjectConfig | null = null; + private userConfig: SensayConfig | null = null; + private configLoaded = false; + + constructor(folderPath: string = '.', options: { silent?: boolean; nonInteractive?: boolean } = {}) { + this.folderPath = folderPath; + this.silent = !!(options.silent || options.nonInteractive); + } + + private async loadConfig(): Promise { + if (this.configLoaded) return; + const { userConfig, projectConfig } = await ConfigManager.getMergedConfig(this.folderPath); + this.userConfig = userConfig; + this.projectConfig = projectConfig; + this.configLoaded = true; + } + + async resolve(params: ResolveParams): Promise { + await this.loadConfig(); + + const { key, cliValue, envVar, promptMessage, isRequired, validator, defaultValue } = params; + + // Priority 1: CLI argument + if (cliValue !== undefined && cliValue !== null) return cliValue; + + // Priority 2: Environment variable + if (envVar) { + const envValue = process.env[envVar]; + if (envValue !== undefined) return envValue; + } + + // Priority 3/4: Project config, then user config + const projectValue = this.projectConfig ? (this.projectConfig as any)[key] : undefined; + const userValue = this.userConfig ? (this.userConfig as any)[key] : undefined; + const configValue = projectValue ?? userValue; + + // In silent mode: use config value or default, error if required and missing + if (this.silent) { + const value = configValue ?? defaultValue; + if (isRequired && (value === undefined || value === null || value === '')) { + throw new Error(`Missing required option: ${key}. Provide it via CLI argument, environment variable, or config file.`); + } + return value; + } + + // Interactive mode: prompt with config value as default + const promptDefault = configValue ?? defaultValue; + const { value } = await inquirer.prompt({ + type: 'input', + name: 'value', + message: promptMessage, + default: promptDefault, + validate: (input: string) => { + if (isRequired && input.trim().length === 0) { + return `${key} is required`; + } + if (validator) return validator(input); + return true; + } + }); + + return value; + } + + getProjectConfig(): ProjectConfig | null { + return this.projectConfig; + } +}