44using Spectre . Console ;
55using XafLogicExplainer . Cli . Helpers ;
66using XafLogicExplainer . Cli . Models ;
7+ using XafLogicExplainer . CopilotSync . Ai ;
78using XafLogicExplainer . CopilotSync . Models ;
89using XafLogicExplainer . CopilotSync . Services ;
910using XafLogicExplainer . Core . Analyzers ;
4647allOption . AddAlias ( "-a" ) ;
4748var enrichOption = new Option < bool > ( "--enrich" , "Enrich controllers with AI-generated business logic summaries" ) ;
4849
50+ // Global, because every command that can enrich needs them and none of them owns the choice of
51+ // model. Declared as options rather than read out of the environment alone so they appear in
52+ // --help: the reason nobody outside this shop could use --enrich was that nothing said how.
53+ var apiKeyOption = new Option < string ? > ( "--api-key" , "API key for the AI provider (or set OPENAI_API_KEY / ANTHROPIC_API_KEY)" ) ;
54+ var aiBaseUrlOption = new Option < string ? > ( "--ai-base-url" , "Any OpenAI-compatible endpoint, including a local one" ) ;
55+ var aiModelOption = new Option < string ? > ( "--ai-model" , "Model name, when not the provider's default" ) ;
56+
4957// ============================================================
5058// ROOT COMMAND
5159// ============================================================
5260
5361var rootCommand = new RootCommand ( "XAF Logic Explainer CLI - Extract and sync XAF project documentation" ) ;
5462
63+ rootCommand . AddGlobalOption ( apiKeyOption ) ;
64+ rootCommand . AddGlobalOption ( aiBaseUrlOption ) ;
65+ rootCommand . AddGlobalOption ( aiModelOption ) ;
66+
67+ // Read once, here, rather than threaded through each handler: SetHandler takes at most eight
68+ // parameters and extract already spends six, so adding three would have forced four commands onto
69+ // the InvocationContext pattern to carry a setting none of them decides.
70+ var aiParse = rootCommand . Parse ( args ) ;
71+ var aiOverrides = new AiClientRequest
72+ {
73+ ApiKey = aiParse . GetValueForOption ( apiKeyOption ) ,
74+ BaseUrl = aiParse . GetValueForOption ( aiBaseUrlOption ) ,
75+ Model = aiParse . GetValueForOption ( aiModelOption ) ,
76+ } ;
77+
5578// ============================================================
5679// COMMAND: config
5780// ============================================================
@@ -380,7 +403,7 @@ await AnsiConsole.Status().Spinner(Spinner.Known.Dots).StartAsync("Extracting pr
380403
381404 if ( enrich )
382405 {
383- await EnrichWithAi ( project , config , language ! ) ;
406+ await EnrichWithAi ( project , config , language ! , aiOverrides ) ;
384407 }
385408
386409 // Summary table
@@ -631,7 +654,7 @@ await AnsiConsole.Status().Spinner(Spinner.Known.Dots).StartAsync("Syncing to Pe
631654 {
632655 enrichHook = async ( extractedProject ) =>
633656 {
634- await EnrichWithAi ( extractedProject , config , language ! ) ;
657+ await EnrichWithAi ( extractedProject , config , language ! , aiOverrides ) ;
635658 } ;
636659 }
637660 singleResult = await syncService . SyncAsync ( singleSyncConfig , BuildExtractionOptions ( language , orm ) , msg =>
@@ -1433,7 +1456,8 @@ await GenerateAgentFiles(
14331456 agentsForce ,
14341457 agentsEnrich ,
14351458 agentsOptions ,
1436- agentsConfig ) ;
1459+ agentsConfig ,
1460+ aiOverrides ) ;
14371461 }
14381462
14391463 return ;
@@ -1454,7 +1478,8 @@ await GenerateAgentFiles(
14541478 agentsForce ,
14551479 agentsEnrich ,
14561480 agentsOptions ,
1457- agentsConfig ) ;
1481+ agentsConfig ,
1482+ aiOverrides ) ;
14581483} ) ;
14591484
14601485rootCommand . AddCommand ( agentsCommand ) ;
@@ -1503,7 +1528,7 @@ await AnsiConsole.Status().Spinner(Spinner.Known.Dots).StartAsync("Reading the a
15031528
15041529 if ( explainEnrich )
15051530 {
1506- await EnrichWithAi ( explained , explainConfig , explainLang ) ;
1531+ await EnrichWithAi ( explained , explainConfig , explainLang , aiOverrides ) ;
15071532 }
15081533
15091534 var html = new HtmlExplainerGenerator ( ThisAssemblyVersion ( ) ) . Generate ( explained ) ;
@@ -1759,7 +1784,8 @@ static async Task GenerateAgentFiles(
17591784 bool force ,
17601785 bool enrich ,
17611786 AgentFilesOptions options ,
1762- CliConfig config )
1787+ CliConfig config ,
1788+ AiClientRequest aiOverrides )
17631789{
17641790 if ( ! Directory . Exists ( projectPath ) )
17651791 {
@@ -1784,7 +1810,7 @@ await AnsiConsole.Status().Spinner(Spinner.Known.Dots).StartAsync("Reading the a
17841810
17851811 if ( enrich )
17861812 {
1787- await EnrichWithAi ( agentProject , config , language ) ;
1813+ await EnrichWithAi ( agentProject , config , language , aiOverrides ) ;
17881814 }
17891815
17901816 var agentGenerator = new MarkdownDocumentationGenerator ( language ) ;
@@ -1967,49 +1993,46 @@ static void DisplayDiffSummary(ProjectDiffReport report)
19671993// HELPER: AI Business Logic Enrichment
19681994// ============================================================
19691995
1970- static async Task EnrichWithAi ( ExtractedProject project , CliConfig config , string language )
1996+ static async Task EnrichWithAi (
1997+ ExtractedProject project , CliConfig config , string language , AiClientRequest aiOverrides )
19711998{
19721999 if ( project . Controllers . Count == 0 )
19732000 {
19742001 AnsiConsole . MarkupLine ( "[grey] No controllers to enrich.[/]" ) ;
19752002 return ;
19762003 }
19772004
1978- var apiUrl = config . ApiUrl ;
1979- var token = config . Token ;
1980- var userName = config . UserName ?? "xaf-logic-explainer" ;
1981- var resourceName = config . ResourceName ?? "" ;
1982-
1983- if ( string . IsNullOrEmpty ( apiUrl ) || string . IsNullOrEmpty ( token ) )
2005+ // A PeopleWorks Copilot account is now the last of several routes rather than the only one, so
2006+ // it is offered rather than required: an empty URL or token simply means this route is not
2007+ // configured, and the resolver moves on to the next.
2008+ var request = aiOverrides with
19842009 {
1985- AnsiConsole . MarkupLine ( "[yellow] --enrich requires API credentials. Configure with: xaflogic config[/]" ) ;
1986- return ;
1987- }
2010+ Copilot = new SyncConfiguration
2011+ {
2012+ CopilotApiBaseUrl = config . ApiUrl ?? "" ,
2013+ CopilotApiToken = config . Token ?? "" ,
2014+ UserName = config . UserName ?? "xaf-logic-explainer" ,
2015+ ResourceName = config . ResourceName ?? "" ,
2016+ } ,
2017+ } ;
19882018
1989- AnsiConsole . MarkupLine ( "[grey] Fetching AI provider credentials...[/]" ) ;
1990- AiProviderInfo ? aiProvider = null ;
1991- using ( var apiClient = new CopilotApiClient ( apiUrl , token , userName , resourceName ) )
1992- {
1993- aiProvider = await apiClient . GetAiProviderAsync ( ) ;
1994- }
2019+ var resolved = await AiClientResolver . ResolveAsync ( request ) ;
19952020
1996- if ( aiProvider == null || string . IsNullOrEmpty ( aiProvider . ApiKey ) )
2021+ if ( ! resolved . Succeeded )
19972022 {
1998- AnsiConsole . MarkupLine ( "[yellow] Could not retrieve AI provider. Skipping enrichment.[/]" ) ;
2023+ var problem = resolved . Problem ?? AiClientResolver . NothingConfigured ;
2024+
2025+ foreach ( var line in problem . Split ( '\n ' ) )
2026+ AnsiConsole . MarkupLine ( $ "[yellow] { Markup . Escape ( line ) } [/]") ;
2027+
19992028 return ;
20002029 }
20012030
2002- var model = aiProvider . Parameters ? . GetValueOrDefault ( "model" ) ? . ToString ( ) ?? "gpt-4o-mini" ;
2003- var baseUrl = aiProvider . AiProviderBaseUrl ?? "https://api.openai.com/v1" ;
2031+ var chatClient = resolved . Client ! ;
20042032
2005- AnsiConsole . MarkupLine ( $ "[blue]AI:[/] { Markup . Escape ( aiProvider . ProviderName ?? "OpenAI" ) } / { Markup . Escape ( model ) } ") ;
2033+ AnsiConsole . MarkupLine ( $ "[blue]AI:[/] { Markup . Escape ( resolved . ProviderName ) } / { Markup . Escape ( resolved . Model ) } ") ;
20062034 AnsiConsole . MarkupLine ( $ "[blue] Enriching { project . Controllers . Count } controllers...[/]") ;
20072035
2008- var openAiClient = new OpenAIClient (
2009- new System . ClientModel . ApiKeyCredential ( aiProvider . ApiKey ) ,
2010- new OpenAIClientOptions { Endpoint = new Uri ( baseUrl ) } ) ;
2011- var chatClient = openAiClient . GetChatClient ( model ) . AsIChatClient ( ) ;
2012-
20132036 var enricher = new BusinessLogicEnricher ( chatClient ) ;
20142037 await enricher . EnrichAsync ( project , language , msg =>
20152038 {
0 commit comments