2026-03-08 16:43:34 +05:30
import path from "node:path" ;
import type {
AdapterEnvironmentCheck ,
AdapterEnvironmentTestContext ,
AdapterEnvironmentTestResult ,
} from "@paperclipai/adapter-utils" ;
import {
asBoolean ,
asString ,
asStringArray ,
ensureAbsoluteDirectory ,
ensureCommandResolvable ,
ensurePathInEnv ,
parseObject ,
runChildProcess ,
} from "@paperclipai/adapter-utils/server-utils" ;
import { DEFAULT_GEMINI_LOCAL_MODEL } from "../index.js" ;
2026-03-09 15:16:15 +00:00
import { detectGeminiAuthRequired , parseGeminiJsonl } from "./parse.js" ;
2026-03-08 19:20:43 +05:30
import { firstNonEmptyLine } from "./utils.js" ;
2026-03-08 16:43:34 +05:30
function summarizeStatus ( checks : AdapterEnvironmentCheck [ ] ) : AdapterEnvironmentTestResult [ "status" ] {
if ( checks . some ( ( check ) = > check . level === "error" ) ) return "fail" ;
if ( checks . some ( ( check ) = > check . level === "warn" ) ) return "warn" ;
return "pass" ;
}
function isNonEmpty ( value : unknown ) : value is string {
return typeof value === "string" && value . trim ( ) . length > 0 ;
}
function commandLooksLike ( command : string , expected : string ) : boolean {
const base = path . basename ( command ) . toLowerCase ( ) ;
return base === expected || base === ` ${ expected } .cmd ` || base === ` ${ expected } .exe ` ;
}
function summarizeProbeDetail ( stdout : string , stderr : string , parsedError : string | null ) : string | null {
const raw = parsedError ? . trim ( ) || firstNonEmptyLine ( stderr ) || firstNonEmptyLine ( stdout ) ;
if ( ! raw ) return null ;
const clean = raw . replace ( /\s+/g , " " ) . trim ( ) ;
const max = 240 ;
return clean . length > max ? ` ${ clean . slice ( 0 , max - 1 ) } … ` : clean ;
}
export async function testEnvironment (
ctx : AdapterEnvironmentTestContext ,
) : Promise < AdapterEnvironmentTestResult > {
const checks : AdapterEnvironmentCheck [ ] = [ ] ;
const config = parseObject ( ctx . config ) ;
const command = asString ( config . command , "gemini" ) ;
const cwd = asString ( config . cwd , process . cwd ( ) ) ;
try {
await ensureAbsoluteDirectory ( cwd , { createIfMissing : true } ) ;
checks . push ( {
code : "gemini_cwd_valid" ,
level : "info" ,
message : ` Working directory is valid: ${ cwd } ` ,
} ) ;
} catch ( err ) {
checks . push ( {
code : "gemini_cwd_invalid" ,
level : "error" ,
message : err instanceof Error ? err . message : "Invalid working directory" ,
detail : cwd ,
} ) ;
}
const envConfig = parseObject ( config . env ) ;
const env : Record < string , string > = { } ;
for ( const [ key , value ] of Object . entries ( envConfig ) ) {
if ( typeof value === "string" ) env [ key ] = value ;
}
const runtimeEnv = ensurePathInEnv ( { . . . process . env , . . . env } ) ;
try {
await ensureCommandResolvable ( command , cwd , runtimeEnv ) ;
checks . push ( {
code : "gemini_command_resolvable" ,
level : "info" ,
message : ` Command is executable: ${ command } ` ,
} ) ;
} catch ( err ) {
checks . push ( {
code : "gemini_command_unresolvable" ,
level : "error" ,
message : err instanceof Error ? err . message : "Command is not executable" ,
detail : command ,
} ) ;
}
const configGeminiApiKey = env . GEMINI_API_KEY ;
const hostGeminiApiKey = process . env . GEMINI_API_KEY ;
const configGoogleApiKey = env . GOOGLE_API_KEY ;
const hostGoogleApiKey = process . env . GOOGLE_API_KEY ;
2026-03-09 15:16:15 +00:00
const hasGca = env . GOOGLE_GENAI_USE_GCA === "true" || process . env . GOOGLE_GENAI_USE_GCA === "true" ;
2026-03-08 16:43:34 +05:30
if (
isNonEmpty ( configGeminiApiKey ) ||
isNonEmpty ( hostGeminiApiKey ) ||
isNonEmpty ( configGoogleApiKey ) ||
2026-03-09 15:16:15 +00:00
isNonEmpty ( hostGoogleApiKey ) ||
hasGca
2026-03-08 16:43:34 +05:30
) {
2026-03-09 15:16:15 +00:00
const source = hasGca
? "Google account login (GCA)"
: isNonEmpty ( configGeminiApiKey ) || isNonEmpty ( configGoogleApiKey )
? "adapter config env"
: "server environment" ;
2026-03-08 16:43:34 +05:30
checks . push ( {
code : "gemini_api_key_present" ,
level : "info" ,
message : "Gemini API credentials are set for CLI authentication." ,
detail : ` Detected in ${ source } . ` ,
} ) ;
} else {
checks . push ( {
code : "gemini_api_key_missing" ,
level : "warn" ,
message : "No Gemini API key was detected. Gemini runs may fail until auth is configured." ,
2026-03-09 15:16:15 +00:00
hint : "Set GEMINI_API_KEY or GOOGLE_API_KEY in adapter env/shell, run `gemini auth` / `gemini auth login`, or set GOOGLE_GENAI_USE_GCA=true for Google account auth." ,
2026-03-08 16:43:34 +05:30
} ) ;
}
const canRunProbe =
checks . every ( ( check ) = > check . code !== "gemini_cwd_invalid" && check . code !== "gemini_command_unresolvable" ) ;
if ( canRunProbe ) {
if ( ! commandLooksLike ( command , "gemini" ) ) {
checks . push ( {
code : "gemini_hello_probe_skipped_custom_command" ,
level : "info" ,
message : "Skipped hello probe because command is not `gemini`." ,
detail : command ,
hint : "Use the `gemini` CLI command to run the automatic installation and auth probe." ,
} ) ;
} else {
const model = asString ( config . model , DEFAULT_GEMINI_LOCAL_MODEL ) . trim ( ) ;
const yolo = asBoolean ( config . yolo , false ) ;
const extraArgs = ( ( ) = > {
const fromExtraArgs = asStringArray ( config . extraArgs ) ;
if ( fromExtraArgs . length > 0 ) return fromExtraArgs ;
return asStringArray ( config . args ) ;
} ) ( ) ;
2026-03-08 19:20:43 +05:30
const args = [ "--output-format" , "stream-json" ] ;
2026-03-08 16:43:34 +05:30
if ( model && model !== DEFAULT_GEMINI_LOCAL_MODEL ) args . push ( "--model" , model ) ;
if ( yolo ) args . push ( "--approval-mode" , "yolo" ) ;
if ( extraArgs . length > 0 ) args . push ( . . . extraArgs ) ;
args . push ( "Respond with hello." ) ;
const probe = await runChildProcess (
` gemini-envtest- ${ Date . now ( ) } - ${ Math . random ( ) . toString ( 16 ) . slice ( 2 ) } ` ,
command ,
args ,
{
cwd ,
env ,
timeoutSec : 45 ,
graceSec : 5 ,
2026-03-08 19:20:43 +05:30
onLog : async ( ) = > { } ,
2026-03-08 16:43:34 +05:30
} ,
) ;
const parsed = parseGeminiJsonl ( probe . stdout ) ;
const detail = summarizeProbeDetail ( probe . stdout , probe . stderr , parsed . errorMessage ) ;
2026-03-09 15:16:15 +00:00
const authMeta = detectGeminiAuthRequired ( {
parsed : parsed.resultEvent ,
stdout : probe.stdout ,
stderr : probe.stderr ,
} ) ;
2026-03-08 16:43:34 +05:30
if ( probe . timedOut ) {
checks . push ( {
code : "gemini_hello_probe_timed_out" ,
level : "warn" ,
message : "Gemini hello probe timed out." ,
hint : "Retry the probe. If this persists, verify Gemini can run `Respond with hello.` from this directory manually." ,
} ) ;
} else if ( ( probe . exitCode ? ? 1 ) === 0 ) {
const summary = parsed . summary . trim ( ) ;
const hasHello = /\bhello\b/i . test ( summary ) ;
checks . push ( {
code : hasHello ? "gemini_hello_probe_passed" : "gemini_hello_probe_unexpected_output" ,
level : hasHello ? "info" : "warn" ,
message : hasHello
? "Gemini hello probe succeeded."
: "Gemini probe ran but did not return `hello` as expected." ,
. . . ( summary ? { detail : summary.replace ( /\s+/g , " " ) . trim ( ) . slice ( 0 , 240 ) } : { } ) ,
. . . ( hasHello
? { }
: {
2026-03-08 19:20:43 +05:30
hint : "Try `gemini --output-format json \"Respond with hello.\"` manually to inspect full output." ,
} ) ,
2026-03-08 16:43:34 +05:30
} ) ;
2026-03-09 15:16:15 +00:00
} else if ( authMeta . requiresAuth ) {
2026-03-08 16:43:34 +05:30
checks . push ( {
code : "gemini_hello_probe_auth_required" ,
level : "warn" ,
message : "Gemini CLI is installed, but authentication is not ready." ,
. . . ( detail ? { detail } : { } ) ,
hint : "Run `gemini auth` or configure GEMINI_API_KEY / GOOGLE_API_KEY in adapter env/shell, then retry the probe." ,
} ) ;
} else {
checks . push ( {
code : "gemini_hello_probe_failed" ,
level : "error" ,
message : "Gemini hello probe failed." ,
. . . ( detail ? { detail } : { } ) ,
hint : "Run `gemini --output-format json \"Respond with hello.\"` manually in this working directory to debug." ,
} ) ;
}
}
}
return {
adapterType : ctx.adapterType ,
status : summarizeStatus ( checks ) ,
checks ,
testedAt : new Date ( ) . toISOString ( ) ,
} ;
}