@@ -16,7 +16,10 @@ import { statusTool } from "./tools/status.js";
1616import { detectWorkspace } from "./utils/workspace-detector.js" ;
1717import { atomicWrite , ensureDir } from "./storage/engine.js" ;
1818import { saveMemory , toMemorySlug } from "./storage/memory.js" ;
19- import type { WorkspaceInfo } from "./types.js" ;
19+ import { detectAuthOptions } from "./utils/auth-detect.js" ;
20+ import { authConfigPath , loadAuthConfig , saveAuthConfig } from "./utils/auth-config.js" ;
21+ import { formatDetectionBlock , hasAnyAuth , promptAuthChoice } from "./utils/auth-prompt.js" ;
22+ import type { AuthMode , WorkspaceInfo } from "./types.js" ;
2023import { AXME_CODE_DIR } from "./types.js" ;
2124
2225const args = process . argv . slice ( 2 ) ;
@@ -150,6 +153,49 @@ function hasAuth(): boolean {
150153 return false ;
151154}
152155
156+ /**
157+ * Print detection block + saved choice (if any).
158+ */
159+ function printAuthStatus ( ) : void {
160+ const options = detectAuthOptions ( ) ;
161+ console . log ( formatDetectionBlock ( options ) ) ;
162+ const saved = loadAuthConfig ( ) ;
163+ if ( saved ) {
164+ console . log ( `\nCurrent mode: ${ saved . mode } (saved ${ saved . chosenAt } )` ) ;
165+ console . log ( `Config file: ${ authConfigPath ( ) } ` ) ;
166+ } else {
167+ console . log ( "\nCurrent mode: not configured (using heuristic fallback)" ) ;
168+ console . log ( `Config file: ${ authConfigPath ( ) } (will be created on first choice)` ) ;
169+ }
170+ }
171+
172+ /**
173+ * Called from `axme-code setup` before LLM scanners launch. Persists a user
174+ * choice to ~/.config/axme-code/auth.yaml exactly once on first setup, so
175+ * subsequent runs (and all MCP server scanner/auditor subprocesses) use the
176+ * same mode without re-prompting. In non-TTY contexts (CI, scripts) we skip
177+ * the prompt and let `resolveAuthMode()` fall back to its heuristic — we do
178+ * NOT persist a guessed choice silently.
179+ */
180+ async function ensureAuthConfiguredForSetup ( ) : Promise < void > {
181+ if ( loadAuthConfig ( ) ) return ;
182+ if ( ! process . stdin . isTTY ) return ;
183+
184+ const options = detectAuthOptions ( ) ;
185+ if ( ! hasAnyAuth ( options ) ) return ; // hasAuth preflight will fail anyway
186+
187+ console . log ( "\nAuthentication setup for LLM scanners" ) ;
188+ console . log ( formatDetectionBlock ( options ) ) ;
189+ console . log ( "" ) ;
190+ const choice = await promptAuthChoice ( options ) ;
191+ if ( ! choice ) {
192+ console . log ( " Auth selection cancelled. Heuristic fallback will be used." ) ;
193+ return ;
194+ }
195+ saveAuthConfig ( choice ) ;
196+ console . log ( ` Saved auth mode: ${ choice } (${ authConfigPath ( ) } )` ) ;
197+ }
198+
153199function generateWorkspaceYaml ( workspacePath : string , ws : WorkspaceInfo ) : void {
154200 const wsYaml = yaml . dump ( {
155201 name : workspacePath . split ( "/" ) . pop ( ) ,
@@ -256,6 +302,9 @@ Usage:
256302 axme-code setup [path] [--force] Initialize project (LLM scan + .mcp.json + CLAUDE.md)
257303 axme-code serve Start MCP server (stdio transport)
258304 axme-code status [path] Show project status
305+ axme-code auth Re-detect and choose auth mode (subscription/api_key)
306+ axme-code auth status Show current auth mode + detected options
307+ axme-code auth use <subscription|api_key> Set auth mode non-interactively
259308 axme-code cleanup legacy-artifacts [--dry-run] Remove pre-PR#7 sessions/logs
260309 axme-code cleanup decisions-normalize [--dry-run] Add status:active to decisions
261310 axme-code audit-kb [path] [--all-repos] KB audit: dedup, conflicts, compaction
@@ -339,6 +388,11 @@ async function main() {
339388 process . exit ( 1 ) ;
340389 }
341390
391+ // Auth mode selection — prompt once on first interactive setup, persist
392+ // to ~/.config/axme-code/auth.yaml. Later runs and scanner subprocesses
393+ // read the saved mode via resolveAuthMode() in buildAgentEnv().
394+ await ensureAuthConfiguredForSetup ( ) ;
395+
342396 // Init with LLM scanners (parallel)
343397 try {
344398 if ( isWorkspace ) {
@@ -649,6 +703,51 @@ Do NOT skip — without context you will miss critical project rules.
649703 break ;
650704 }
651705
706+ case "auth" : {
707+ const sub = args [ 1 ] ;
708+ if ( sub === "status" || sub === "show" ) {
709+ printAuthStatus ( ) ;
710+ break ;
711+ }
712+ if ( sub === "use" || sub === "set" ) {
713+ const mode = args [ 2 ] ;
714+ if ( mode !== "subscription" && mode !== "api_key" ) {
715+ console . error ( "Usage: axme-code auth use <subscription|api_key>" ) ;
716+ process . exit ( 1 ) ;
717+ }
718+ saveAuthConfig ( mode as AuthMode ) ;
719+ console . log ( `Saved auth mode: ${ mode } (${ authConfigPath ( ) } )` ) ;
720+ break ;
721+ }
722+ if ( sub === undefined || sub === "choose" ) {
723+ const options = detectAuthOptions ( ) ;
724+ console . log ( "Authentication setup for LLM scanners" ) ;
725+ console . log ( formatDetectionBlock ( options ) ) ;
726+ const saved = loadAuthConfig ( ) ;
727+ if ( saved ) console . log ( `\nCurrent mode: ${ saved . mode } (saved ${ saved . chosenAt } )` ) ;
728+ console . log ( "" ) ;
729+ if ( ! hasAnyAuth ( options ) ) {
730+ console . error ( "No authentication detected. Set ANTHROPIC_API_KEY or run `claude /login`, then re-run `axme-code auth`." ) ;
731+ process . exit ( 1 ) ;
732+ }
733+ if ( ! process . stdin . isTTY ) {
734+ console . error ( "`axme-code auth` requires an interactive terminal. Use `axme-code auth use <subscription|api_key>` non-interactively." ) ;
735+ process . exit ( 1 ) ;
736+ }
737+ const choice = await promptAuthChoice ( options ) ;
738+ if ( ! choice ) {
739+ console . log ( "Cancelled. No change." ) ;
740+ break ;
741+ }
742+ saveAuthConfig ( choice ) ;
743+ console . log ( `Saved auth mode: ${ choice } (${ authConfigPath ( ) } )` ) ;
744+ break ;
745+ }
746+ console . error ( `Unknown 'auth' subcommand: ${ sub } ` ) ;
747+ console . error ( "Available: (none)|choose, status|show, use|set <subscription|api_key>" ) ;
748+ process . exit ( 1 ) ;
749+ }
750+
652751 case "help" :
653752 case "--help" :
654753 case "-h" :
0 commit comments