diff --git a/bun.lock b/bun.lock index fd6df7f..7b9dbf0 100644 --- a/bun.lock +++ b/bun.lock @@ -1,11 +1,11 @@ { "lockfileVersion": 1, - "configVersion": 1, "workspaces": { "": { "name": "@google/gemini-api-cli", "dependencies": { "citty": "^0.1.6", + "dotenv": "^17.4.2", "js-yaml": "^4.1.0", "zod": "^3.24.0", }, @@ -102,6 +102,8 @@ "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], + "dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="], + "esbuild": ["esbuild@0.28.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.0", "@esbuild/android-arm": "0.28.0", "@esbuild/android-arm64": "0.28.0", "@esbuild/android-x64": "0.28.0", "@esbuild/darwin-arm64": "0.28.0", "@esbuild/darwin-x64": "0.28.0", "@esbuild/freebsd-arm64": "0.28.0", "@esbuild/freebsd-x64": "0.28.0", "@esbuild/linux-arm": "0.28.0", "@esbuild/linux-arm64": "0.28.0", "@esbuild/linux-ia32": "0.28.0", "@esbuild/linux-loong64": "0.28.0", "@esbuild/linux-mips64el": "0.28.0", "@esbuild/linux-ppc64": "0.28.0", "@esbuild/linux-riscv64": "0.28.0", "@esbuild/linux-s390x": "0.28.0", "@esbuild/linux-x64": "0.28.0", "@esbuild/netbsd-arm64": "0.28.0", "@esbuild/netbsd-x64": "0.28.0", "@esbuild/openbsd-arm64": "0.28.0", "@esbuild/openbsd-x64": "0.28.0", "@esbuild/openharmony-arm64": "0.28.0", "@esbuild/sunos-x64": "0.28.0", "@esbuild/win32-arm64": "0.28.0", "@esbuild/win32-ia32": "0.28.0", "@esbuild/win32-x64": "0.28.0" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw=="], "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], diff --git a/package-lock.json b/package-lock.json index df6732f..47c6a84 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,16 @@ { "name": "@google/gemini-api-cli", - "version": "0.1.0", + "version": "0.2.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@google/gemini-api-cli", - "version": "0.1.0", + "version": "0.2.1", "license": "Apache-2.0", "dependencies": { "citty": "^0.1.6", + "dotenv": "^17.4.2", "js-yaml": "^4.1.0", "zod": "^3.24.0" }, @@ -21,6 +22,7 @@ "@types/js-yaml": "^4.0.9", "@types/node": "^22.0.0", "bun-types": "latest", + "esbuild": "latest", "typescript": "^5.7.0" }, "engines": { @@ -186,6 +188,448 @@ "node": ">=14.21.3" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", + "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", + "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", + "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", + "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", + "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", + "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", + "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", + "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", + "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", + "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", + "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", + "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", + "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", + "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", + "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", + "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", + "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", + "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", + "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", + "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", + "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", + "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", + "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", + "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", + "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", + "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@types/js-yaml": { "version": "4.0.9", "dev": true, @@ -227,6 +671,60 @@ "node": "^14.18.0 || >=16.10.0" } }, + "node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/esbuild": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", + "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.0", + "@esbuild/android-arm": "0.28.0", + "@esbuild/android-arm64": "0.28.0", + "@esbuild/android-x64": "0.28.0", + "@esbuild/darwin-arm64": "0.28.0", + "@esbuild/darwin-x64": "0.28.0", + "@esbuild/freebsd-arm64": "0.28.0", + "@esbuild/freebsd-x64": "0.28.0", + "@esbuild/linux-arm": "0.28.0", + "@esbuild/linux-arm64": "0.28.0", + "@esbuild/linux-ia32": "0.28.0", + "@esbuild/linux-loong64": "0.28.0", + "@esbuild/linux-mips64el": "0.28.0", + "@esbuild/linux-ppc64": "0.28.0", + "@esbuild/linux-riscv64": "0.28.0", + "@esbuild/linux-s390x": "0.28.0", + "@esbuild/linux-x64": "0.28.0", + "@esbuild/netbsd-arm64": "0.28.0", + "@esbuild/netbsd-x64": "0.28.0", + "@esbuild/openbsd-arm64": "0.28.0", + "@esbuild/openbsd-x64": "0.28.0", + "@esbuild/openharmony-arm64": "0.28.0", + "@esbuild/sunos-x64": "0.28.0", + "@esbuild/win32-arm64": "0.28.0", + "@esbuild/win32-ia32": "0.28.0", + "@esbuild/win32-x64": "0.28.0" + } + }, "node_modules/js-yaml": { "version": "4.1.1", "license": "MIT", diff --git a/package.json b/package.json index a9d6d86..dcca08f 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ }, "dependencies": { "citty": "^0.1.6", + "dotenv": "^17.4.2", "js-yaml": "^4.1.0", "zod": "^3.24.0" }, diff --git a/src/commands/agents/create.ts b/src/commands/agents/create.ts index d637da8..1468ae7 100644 --- a/src/commands/agents/create.ts +++ b/src/commands/agents/create.ts @@ -33,17 +33,23 @@ Examples: type: "string", description: "Override base environment", }, + env: { + type: "string", + alias: "e", + description: "Load environment variables from a .env file", + }, }, async run({ args }) { try { - const ctx = resolveContext(args); const agentDir = args.path as string; const baseEnvOverride = args["base-env"] as string | undefined; + const envFile = args.env as string | undefined; + const ctx = resolveContext(args); - const { config } = await loadAgent(agentDir); + const { config } = await loadAgent(agentDir, { envFile }); const body: Record = { - name: config.id, + id: config.id, base_agent: config.base_agent, }; diff --git a/src/commands/agents/test.ts b/src/commands/agents/test.ts index 9cd5549..225a942 100644 --- a/src/commands/agents/test.ts +++ b/src/commands/agents/test.ts @@ -67,12 +67,17 @@ Examples: type: "string", description: "Use existing environment", }, + env: { + type: "string", + alias: "e", + description: "Load environment variables from a .env file", + }, }, async run({ args }) { try { const agentDir = args.path as string; const prompt = args.prompt as string; - + const envFile = args.env as string | undefined; const sharedFlags = { apiKey: (args["api-key"] || args.apiKey) as string | undefined, baseUrl: (args["base-url"] || args.baseUrl) as string | undefined, @@ -82,7 +87,7 @@ Examples: const ctx = resolveContext(sharedFlags); - const { config } = await loadAgent(agentDir); + const { config } = await loadAgent(agentDir, { envFile }); const inlineFiles = await collectInlineFiles(agentDir); // system_instruction from agent.yaml is sent in the request body. diff --git a/src/lib/config.ts b/src/lib/config.ts index d2e958c..76ee151 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -12,8 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. +import { existsSync } from "node:fs"; import { readFile } from "node:fs/promises"; import { join, resolve } from "node:path"; +import { parse as parseDotenv } from "dotenv"; import { ConfigError } from "./errors"; import { type AgentConfig, AgentConfigSchema } from "./schemas"; import { parseYaml } from "./yaml"; @@ -23,14 +25,66 @@ export interface LoadedAgent { dir: string; } -export async function loadAgent(dir: string): Promise { +export interface LoadAgentOptions { + envFile?: string; +} + +async function loadEnvFile(path: string): Promise> { + const envPath = resolve(path); + if (!existsSync(envPath)) { + throw new ConfigError(`Environment file not found: ${path}`); + } + try { + return parseDotenv(await readFile(envPath, "utf-8")); + } catch (error) { + throw new ConfigError(`Failed to read environment file ${path}: ${(error as Error).message}`); + } +} + +function resolveEnvVarString(value: string, envFileVars: Record): string { + const names = new Set( + [...value.matchAll(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g)].map((match) => match[1]), + ); + let resolved = value; + + for (const name of names) { + const envValue = envFileVars[name] ?? process.env[name]; + if (envValue === undefined) { + throw new ConfigError(`Missing environment variable ${name}`); + } + resolved = resolved.replaceAll(`\${${name}}`, () => envValue); + } + + return resolved; +} + +function resolveEnvVars(value: unknown, envFileVars: Record): unknown { + if (typeof value === "string") { + return resolveEnvVarString(value, envFileVars); + } + + if (Array.isArray(value)) { + return value.map((item) => resolveEnvVars(item, envFileVars)); + } + + if (value && typeof value === "object") { + return Object.fromEntries( + Object.entries(value).map(([key, child]) => [key, resolveEnvVars(child, envFileVars)]), + ); + } + + return value; +} + +export async function loadAgent(dir: string, options: LoadAgentOptions = {}): Promise { const absDir = resolve(dir); const yamlPath = join(absDir, "agent.yaml"); try { // Read file const raw = await readFile(yamlPath, "utf-8"); - const parsed = parseYaml(raw); + const envFileVars = options.envFile ? await loadEnvFile(options.envFile) : {}; + const parsed = resolveEnvVars(parseYaml(raw), envFileVars); // Validate with Zod const result = AgentConfigSchema.safeParse(parsed); diff --git a/tests/agents/filtering.test.ts b/tests/agents/filtering.test.ts index 1eeee11..47206cd 100644 --- a/tests/agents/filtering.test.ts +++ b/tests/agents/filtering.test.ts @@ -60,6 +60,7 @@ describe("agents create inlining integration", () => { const body = JSON.parse(bodyStr); + expect(body.id).toBe(agentName); expect(body.base_environment).toBeDefined(); expect(body.base_environment.type).toBe("remote"); expect(body.base_environment.sources).toBeDefined(); diff --git a/tests/config.test.ts b/tests/config.test.ts index 854ee25..6636dee 100644 --- a/tests/config.test.ts +++ b/tests/config.test.ts @@ -16,6 +16,14 @@ import { describe, expect, test } from "bun:test"; import { loadAgent } from "../src/lib/config"; import { AgentConfigSchema } from "../src/lib/schemas"; +function restoreEnv(name: string, value: string | undefined) { + if (value === undefined) { + delete process.env[name]; + } else { + process.env[name] = value; + } +} + describe("AgentConfigSchema", () => { test("valid minimal config", () => { const result = AgentConfigSchema.safeParse({ @@ -161,4 +169,126 @@ describe("loadAgent", () => { expect(agent.config.examples).toHaveLength(2); expect(agent.config.examples?.[0].title).toBe("Write a poem"); }); + + test("resolves agent.yaml environment variable placeholders from process env", async () => { + const oldGithubToken = process.env.GITHUB_TOKEN; + const oldGithubPat = process.env.GITHUB_PAT; + const oldGeminiApiKey = process.env.GEMINI_API_KEY; + try { + process.env.GITHUB_TOKEN = 'process-"github"-token'; + process.env.GITHUB_PAT = "process-$&-github-pat"; + process.env.GEMINI_API_KEY = "process-gemini-api-key"; + + const agent = await loadAgent("./tests/fixtures/agent-configs/with-env-vars"); + expect(agent.config.id).toBe("test-agent-with-env-vars"); + expect(agent.config.sources).toEqual([ + { + type: "github", + source: "https://process-$&-github-pat@github.com/my-org/private-repo", + target: "/workspace/private-repo", + }, + ]); + expect(agent.config.environment).toEqual({ + type: "remote", + network: { + allowlist: [ + { + domain: "api.github.com", + transform: { + Authorization: 'Bearer process-"github"-token', + "X-GitHub-Token": 'process-"github"-token', + }, + }, + { + domain: "generativelanguage.googleapis.com", + transform: { + "x-goog-api-key": "process-gemini-api-key", + Authorization: "Bearer process-gemini-api-key", + }, + }, + ], + }, + }); + } finally { + restoreEnv("GITHUB_TOKEN", oldGithubToken); + restoreEnv("GITHUB_PAT", oldGithubPat); + restoreEnv("GEMINI_API_KEY", oldGeminiApiKey); + } + }); + + test("resolves agent.yaml environment variable placeholders from env file first", async () => { + const oldGithubToken = process.env.GITHUB_TOKEN; + const oldGithubPat = process.env.GITHUB_PAT; + const oldGeminiApiKey = process.env.GEMINI_API_KEY; + try { + process.env.GITHUB_TOKEN = "process-github-token"; + process.env.GITHUB_PAT = "process-github-pat"; + process.env.GEMINI_API_KEY = "process-gemini-api-key"; + + const agent = await loadAgent("./tests/fixtures/agent-configs/with-env-vars", { + envFile: "./tests/fixtures/agent-configs/with-env-vars/.env", + }); + expect(agent.config.sources).toEqual([ + { + type: "github", + source: "https://env-file-github-pat@github.com/my-org/private-repo", + target: "/workspace/private-repo", + }, + ]); + expect(agent.config.environment).toEqual({ + type: "remote", + network: { + allowlist: [ + { + domain: "api.github.com", + transform: { + Authorization: "Bearer env-file-github-token", + "X-GitHub-Token": "env-file-github-token", + }, + }, + { + domain: "generativelanguage.googleapis.com", + transform: { + "x-goog-api-key": "env-file-gemini-api-key", + Authorization: "Bearer env-file-gemini-api-key", + }, + }, + ], + }, + }); + } finally { + restoreEnv("GITHUB_TOKEN", oldGithubToken); + restoreEnv("GITHUB_PAT", oldGithubPat); + restoreEnv("GEMINI_API_KEY", oldGeminiApiKey); + } + }); + + test("throws when agent.yaml references missing environment variables", async () => { + const oldGithubToken = process.env.GITHUB_TOKEN; + const oldGithubPat = process.env.GITHUB_PAT; + const oldGeminiApiKey = process.env.GEMINI_API_KEY; + try { + delete process.env.GITHUB_TOKEN; + delete process.env.GITHUB_PAT; + delete process.env.GEMINI_API_KEY; + + await expect(loadAgent("./tests/fixtures/agent-configs/with-env-vars")).rejects.toThrow( + "Missing environment variable GITHUB_PAT", + ); + } finally { + restoreEnv("GITHUB_TOKEN", oldGithubToken); + restoreEnv("GITHUB_PAT", oldGithubPat); + restoreEnv("GEMINI_API_KEY", oldGeminiApiKey); + } + }); + + test("throws a clear error when env file is missing", async () => { + await expect( + loadAgent("./tests/fixtures/agent-configs/valid", { + envFile: "./tests/fixtures/agent-configs/valid/missing.env", + }), + ).rejects.toThrow( + "Environment file not found: ./tests/fixtures/agent-configs/valid/missing.env", + ); + }); }); diff --git a/tests/e2e/E2E.md b/tests/e2e/E2E.md index 29781c5..44951d4 100644 --- a/tests/e2e/E2E.md +++ b/tests/e2e/E2E.md @@ -407,7 +407,7 @@ $CLI agents init "$AGENT_NAME" $CLI agents create --path "./$AGENT_NAME" --dry-run ``` -**Assert:** Output contains `curl -X POST`, `/agents`, `"name":`, `"base_agent": "antigravity-preview-05-2026"`. +**Assert:** Output contains `curl -X POST`, `/agents`, `"id":`, `"base_agent": "antigravity-preview-05-2026"`. --- @@ -796,4 +796,3 @@ $CLI agents test --prompt "Hello" --path "./test-agent" --dry-run | [43](./cuj_43.sh) | Interaction logging | UX | | [44](./cuj_44.sh) | Invalid agent | Error | | [45](./cuj_45.sh) | Network transform dry-run | Agent Run | - diff --git a/tests/fixtures/agent-configs/with-env-vars/.env b/tests/fixtures/agent-configs/with-env-vars/.env new file mode 100644 index 0000000..187ec8d --- /dev/null +++ b/tests/fixtures/agent-configs/with-env-vars/.env @@ -0,0 +1,3 @@ +GITHUB_TOKEN=env-file-github-token +GITHUB_PAT=env-file-github-pat +GEMINI_API_KEY=env-file-gemini-api-key diff --git a/tests/fixtures/agent-configs/with-env-vars/agent.yaml b/tests/fixtures/agent-configs/with-env-vars/agent.yaml new file mode 100644 index 0000000..93fa030 --- /dev/null +++ b/tests/fixtures/agent-configs/with-env-vars/agent.yaml @@ -0,0 +1,19 @@ +id: test-agent-with-env-vars +base_agent: antigravity-preview-05-2026 +description: Agent with environment variable placeholders +sources: + - type: github + source: "https://${GITHUB_PAT}@github.com/my-org/private-repo" + target: "/workspace/private-repo" +environment: + type: remote + network: + allowlist: + - domain: api.github.com + transform: + Authorization: "Bearer ${GITHUB_TOKEN}" + X-GitHub-Token: "${GITHUB_TOKEN}" + - domain: generativelanguage.googleapis.com + transform: + x-goog-api-key: "${GEMINI_API_KEY}" + Authorization: "Bearer ${GEMINI_API_KEY}"