From 1db0acdc025dd15a83e10448e22ce810bdb52a4d Mon Sep 17 00:00:00 2001 From: Jacob Cable Date: Mon, 9 Jun 2025 18:02:04 +0100 Subject: [PATCH 01/21] feat(cli): add compile script --- genkit-tools/cli/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/genkit-tools/cli/package.json b/genkit-tools/cli/package.json index 6e0a5854d..181544938 100644 --- a/genkit-tools/cli/package.json +++ b/genkit-tools/cli/package.json @@ -17,6 +17,7 @@ "scripts": { "build": "pnpm genversion && tsc", "build:watch": "tsc --watch", + "compile:bun": "bun build dist/bin/genkit.js --compile --minify --outfile dist/bin/genkit", "test": "jest --verbose", "genversion": "genversion -esf src/utils/version.ts" }, From 74ecf903ac1d40afb304391ca3168d192df2e5eb Mon Sep 17 00:00:00 2001 From: Jacob Cable Date: Mon, 9 Jun 2025 18:02:04 +0100 Subject: [PATCH 02/21] fix(cli): replace node child process in genkit ui:start --- genkit-tools/cli/package.json | 1 + genkit-tools/cli/src/cli.ts | 5 +++ genkit-tools/cli/src/commands/ui-start.ts | 6 +-- genkit-tools/cli/src/utils/server-harness.ts | 45 ++++++++++---------- genkit-tools/cli/tsconfig.json | 3 +- genkit-tools/common/package.json | 1 + genkit-tools/pnpm-lock.yaml | 13 ++++++ 7 files changed, 48 insertions(+), 26 deletions(-) diff --git a/genkit-tools/cli/package.json b/genkit-tools/cli/package.json index 181544938..eafca55c0 100644 --- a/genkit-tools/cli/package.json +++ b/genkit-tools/cli/package.json @@ -43,6 +43,7 @@ "@types/inquirer": "^8.1.3", "@types/jest": "^29.5.12", "@types/node": "^20.11.19", + "bun-types": "^1.2.16", "genversion": "^3.2.0", "jest": "^29.7.0", "ts-jest": "^29.1.2", diff --git a/genkit-tools/cli/src/cli.ts b/genkit-tools/cli/src/cli.ts index b725582bd..ef071c5e4 100644 --- a/genkit-tools/cli/src/cli.ts +++ b/genkit-tools/cli/src/cli.ts @@ -32,6 +32,7 @@ import { getPluginCommands, getPluginSubCommand } from './commands/plugins'; import { start } from './commands/start'; import { uiStart } from './commands/ui-start'; import { uiStop } from './commands/ui-stop'; +import { uiStartServer } from './utils/server-harness'; import { version } from './utils/version'; /** @@ -79,6 +80,10 @@ export async function startCLI(): Promise { await record(new RunCommandEvent(commandName)); }); + if (process.argv.includes('__ui:start-server')) { + program.addCommand(uiStartServer); + } + for (const command of commands) program.addCommand(command); for (const command of await getPluginCommands()) program.addCommand(command); diff --git a/genkit-tools/cli/src/commands/ui-start.ts b/genkit-tools/cli/src/commands/ui-start.ts index dd270a441..c99d1dfc5 100644 --- a/genkit-tools/cli/src/commands/ui-start.ts +++ b/genkit-tools/cli/src/commands/ui-start.ts @@ -126,14 +126,14 @@ async function startAndWaitUntilHealthy( serversDir: string ): Promise { return new Promise((resolve, reject) => { - const serverPath = path.join(__dirname, '../utils/server-harness.js'); const child = spawn( - 'node', - [serverPath, port.toString(), serversDir + '/devui.log'], + process.execPath, + ['__ui:start-server', port.toString(), serversDir + '/devui.log'], { stdio: ['ignore', 'ignore', 'ignore'], } ); + // Only print out logs from the child process to debug output. child.on('error', (error) => reject(error)); child.on('exit', (code) => diff --git a/genkit-tools/cli/src/utils/server-harness.ts b/genkit-tools/cli/src/utils/server-harness.ts index 02feeb07c..b78db09db 100644 --- a/genkit-tools/cli/src/utils/server-harness.ts +++ b/genkit-tools/cli/src/utils/server-harness.ts @@ -15,22 +15,14 @@ */ import { startServer } from '@genkit-ai/tools-common/server'; +import { Command } from 'commander'; import fs from 'fs'; import { startManager } from './manager-utils'; -const args = process.argv.slice(2); -const port = Number.parseInt(args[0]) || 4100; -redirectStdoutToFile(args[1]); - -async function start() { - const manager = await startManager(true); - await startServer(manager, port); -} - function redirectStdoutToFile(logFile: string) { - var myLogFileStream = fs.createWriteStream(logFile); + const myLogFileStream = fs.createWriteStream(logFile); - var originalStdout = process.stdout.write; + const originalStdout = process.stdout.write; function writeStdout() { originalStdout.apply(process.stdout, arguments as any); myLogFileStream.write.apply(myLogFileStream, arguments as any); @@ -40,14 +32,23 @@ function redirectStdoutToFile(logFile: string) { process.stderr.write = process.stdout.write; } -process.on('error', (error): void => { - console.log(`Error in tools process: ${error}`); -}); -process.on('uncaughtException', (err, somethingelse) => { - console.log(`Uncaught error in tools process: ${err} ${somethingelse}`); -}); -process.on('unhandledRejection', (reason, p) => { - console.log(`Unhandled rejection in tools process: ${reason}`); -}); - -start(); +export const uiStartServer = new Command('__ui:start-server') + .argument('', 'Port to serve on') + .argument('', 'Log file path') + .action(async (port: string, logFile: string) => { + redirectStdoutToFile(logFile); + + process.on('error', (error): void => { + console.log(`Error in tools process: ${error}`); + }); + process.on('uncaughtException', (err, somethingelse) => { + console.log(`Uncaught error in tools process: ${err} ${somethingelse}`); + }); + process.on('unhandledRejection', (reason, p) => { + console.log(`Unhandled rejection in tools process: ${reason}`); + }); + + const portNum = Number.parseInt(port) || 4100; + const manager = await startManager(true); + await startServer(manager, portNum); + }); diff --git a/genkit-tools/cli/tsconfig.json b/genkit-tools/cli/tsconfig.json index 4496203d6..326dea538 100644 --- a/genkit-tools/cli/tsconfig.json +++ b/genkit-tools/cli/tsconfig.json @@ -2,7 +2,8 @@ "extends": "../tsconfig.base.json", "compilerOptions": { "module": "commonjs", - "outDir": "dist" + "outDir": "dist", + "types": ["bun-types"] }, "include": ["src"] } diff --git a/genkit-tools/common/package.json b/genkit-tools/common/package.json index b5fa05cd0..065ce7ada 100644 --- a/genkit-tools/common/package.json +++ b/genkit-tools/common/package.json @@ -48,6 +48,7 @@ "@types/json-schema": "^7.0.15", "@types/node": "^20.11.19", "@types/uuid": "^9.0.8", + "bun-types": "^1.2.16", "genversion": "^3.2.0", "jest": "^29.7.0", "npm-run-all": "^4.1.5", diff --git a/genkit-tools/pnpm-lock.yaml b/genkit-tools/pnpm-lock.yaml index 7fa94f66f..fa4b614de 100644 --- a/genkit-tools/pnpm-lock.yaml +++ b/genkit-tools/pnpm-lock.yaml @@ -75,6 +75,9 @@ importers: '@types/node': specifier: ^20.11.19 version: 20.19.1 + bun-types: + specifier: ^1.2.16 + version: 1.2.16 genversion: specifier: ^3.2.0 version: 3.2.0 @@ -202,6 +205,9 @@ importers: '@types/uuid': specifier: ^9.0.8 version: 9.0.8 + bun-types: + specifier: ^1.2.16 + version: 1.2.16 genversion: specifier: ^3.2.0 version: 3.2.0 @@ -1247,6 +1253,9 @@ packages: buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + bun-types@1.2.16: + resolution: {integrity: sha512-ciXLrHV4PXax9vHvUrkvun9VPVGOVwbbbBF/Ev1cXz12lyEZMoJpIJABOfPcN9gDJRaiKF9MVbSygLg4NXu3/A==} + bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} @@ -4368,6 +4377,10 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 + bun-types@1.2.16: + dependencies: + '@types/node': 20.19.1 + bytes@3.1.2: {} call-bind-apply-helpers@1.0.2: From 95df2c10429ff8654f39eb7a59aea42dd5ed832d Mon Sep 17 00:00:00 2001 From: Jacob Cable Date: Wed, 18 Jun 2025 12:27:38 +0100 Subject: [PATCH 03/21] fix(cli): handle different runtime error shapes --- genkit-tools/common/src/utils/errors.ts | 141 ++++++++++++++++++++++++ genkit-tools/common/src/utils/utils.ts | 11 +- js/testapps/basic-gemini/src/index.ts | 30 ++--- 3 files changed, 154 insertions(+), 28 deletions(-) create mode 100644 genkit-tools/common/src/utils/errors.ts diff --git a/genkit-tools/common/src/utils/errors.ts b/genkit-tools/common/src/utils/errors.ts new file mode 100644 index 000000000..32f2c39ec --- /dev/null +++ b/genkit-tools/common/src/utils/errors.ts @@ -0,0 +1,141 @@ +// genkit-tools/common/src/utils/errors.ts + +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Checks if an error is a connection refused error across Node.js and Bun runtimes. + * + * Node.js structure: error.cause.code === 'ECONNREFUSED' + * Bun structure: error.code === 'ConnectionRefused' or error.code === 'ECONNRESET' + */ +export function isConnectionRefusedError(error: unknown): boolean { + if (!error) { + return false; + } + + // Handle plain objects with a code property (Bun fetch errors) + if (typeof error === 'object' && 'code' in error) { + const code = (error as any).code; + if ( + code === 'ECONNREFUSED' || // Node.js + code === 'ConnectionRefused' || // Bun + code === 'ECONNRESET' // Connection reset (also indicates server is down) + ) { + return true; + } + } + + // Handle Error instances + if (error instanceof Error) { + // Direct error code + if ('code' in error && typeof error.code === 'string') { + const code = error.code; + if ( + code === 'ECONNREFUSED' || + code === 'ConnectionRefused' || + code === 'ECONNRESET' + ) { + return true; + } + } + + // Node.js style with cause + if ( + 'cause' in error && + error.cause && + typeof error.cause === 'object' && + 'code' in error.cause && + error.cause.code === 'ECONNREFUSED' + ) { + return true; + } + + // Fallback: check error message + if ( + error.message && + (error.message.includes('ECONNREFUSED') || + error.message.includes('Connection refused') || + error.message.includes('ConnectionRefused') || + error.message.includes('connect ECONNREFUSED')) + ) { + return true; + } + } + + return false; +} + +/** + * Gets the error code from an error object, handling both Node.js and Bun styles. + */ +export function getErrorCode(error: unknown): string | undefined { + if (!error) { + return undefined; + } + + // Handle plain objects with a code property + if ( + typeof error === 'object' && + 'code' in error && + typeof (error as any).code === 'string' + ) { + return (error as any).code; + } + + // Handle Error instances + if (error instanceof Error) { + // Direct error code + if ('code' in error && typeof error.code === 'string') { + return error.code; + } + + // Node.js style with cause + if ( + 'cause' in error && + error.cause && + typeof error.cause === 'object' && + 'code' in error.cause && + typeof error.cause.code === 'string' + ) { + return error.cause.code; + } + } + + return undefined; +} + +/** + * Safely extracts error details for logging. + */ +export function getErrorDetails(error: unknown): string { + if (!error) { + return 'Unknown error'; + } + + const code = getErrorCode(error); + + if (error instanceof Error) { + return code ? `${error.message} (${code})` : error.message; + } + + if (typeof error === 'object' && 'message' in error) { + const message = (error as any).message; + return code ? `${message} (${code})` : message; + } + + return String(error); +} diff --git a/genkit-tools/common/src/utils/utils.ts b/genkit-tools/common/src/utils/utils.ts index 9368e22a3..28531fd2a 100644 --- a/genkit-tools/common/src/utils/utils.ts +++ b/genkit-tools/common/src/utils/utils.ts @@ -17,6 +17,7 @@ import * as fs from 'fs/promises'; import * as path from 'path'; import type { Runtime } from '../manager/types'; +import { isConnectionRefusedError } from './errors'; import { logger } from './logger'; export interface DevToolsInfo { @@ -145,10 +146,7 @@ export async function checkServerHealth(url: string): Promise { const response = await fetch(`${url}/api/__health`); return response.status === 200; } catch (error) { - if ( - error instanceof Error && - (error.cause as any).code === 'ECONNREFUSED' - ) { + if (isConnectionRefusedError(error)) { return false; } } @@ -189,10 +187,7 @@ export async function waitUntilUnresponsive( try { const health = await fetch(`${url}/api/__health`); } catch (error) { - if ( - error instanceof Error && - (error.cause as any).code === 'ECONNREFUSED' - ) { + if (isConnectionRefusedError(error)) { return true; } } diff --git a/js/testapps/basic-gemini/src/index.ts b/js/testapps/basic-gemini/src/index.ts index 90ae4da62..df69be93d 100644 --- a/js/testapps/basic-gemini/src/index.ts +++ b/js/testapps/basic-gemini/src/index.ts @@ -22,36 +22,26 @@ const ai = genkit({ plugins: [googleAI(), vertexAI()], }); -const jokeSubjectGenerator = ai.defineTool( - { - name: 'jokeSubjectGenerator', - description: 'Can be called to generate a subject for a joke', - }, - async () => { - return 'banana'; - } -); - export const jokeFlow = ai.defineFlow( { name: 'jokeFlow', - inputSchema: z.void(), - outputSchema: z.any(), + inputSchema: z.object({ subject: z.string() }), + outputSchema: z.object({ joke: z.string() }), }, - async () => { + async ({ subject }) => { const llmResponse = await ai.generate({ model: gemini15Flash, config: { - temperature: 2, - // if desired, model versions can be explicitly set - version: 'gemini-1.5-flash-002', + temperature: 0.7, }, output: { - schema: z.object({ jokeSubject: z.string() }), + schema: z.object({ joke: z.string() }), }, - tools: [jokeSubjectGenerator], - prompt: `come up with a subject to joke about (using the function provided)`, + prompt: `Tell me a really funny joke about ${subject}`, }); - return llmResponse.output; + if (!llmResponse.output) { + throw new Error('oh no!'); + } + return llmResponse.output!; } ); From 7cee1f9e35878cb670abc7a05bc5a510ae0b5b99 Mon Sep 17 00:00:00 2001 From: Jacob Cable Date: Wed, 18 Jun 2025 14:24:45 +0100 Subject: [PATCH 04/21] fix(cli): avoid absolute paths in binary --- genkit-tools/cli/package.json | 2 +- genkit-tools/cli/tsconfig.json | 3 +- genkit-tools/common/package.json | 60 ++++++++++++------------ genkit-tools/common/src/utils/package.ts | 6 +-- genkit-tools/common/tsconfig.json | 3 +- 5 files changed, 37 insertions(+), 37 deletions(-) diff --git a/genkit-tools/cli/package.json b/genkit-tools/cli/package.json index eafca55c0..247d1aaf8 100644 --- a/genkit-tools/cli/package.json +++ b/genkit-tools/cli/package.json @@ -17,7 +17,7 @@ "scripts": { "build": "pnpm genversion && tsc", "build:watch": "tsc --watch", - "compile:bun": "bun build dist/bin/genkit.js --compile --minify --outfile dist/bin/genkit", + "compile:bun": "bun build src/bin/genkit.ts --compile --outfile dist/bin/genkit", "test": "jest --verbose", "genversion": "genversion -esf src/utils/version.ts" }, diff --git a/genkit-tools/cli/tsconfig.json b/genkit-tools/cli/tsconfig.json index 326dea538..367308665 100644 --- a/genkit-tools/cli/tsconfig.json +++ b/genkit-tools/cli/tsconfig.json @@ -3,7 +3,8 @@ "compilerOptions": { "module": "commonjs", "outDir": "dist", - "types": ["bun-types"] + "types": ["bun-types"], + "resolveJsonModule": true }, "include": ["src"] } diff --git a/genkit-tools/common/package.json b/genkit-tools/common/package.json index 065ce7ada..edb532e97 100644 --- a/genkit-tools/common/package.json +++ b/genkit-tools/common/package.json @@ -64,61 +64,61 @@ }, "author": "genkit", "license": "Apache-2.0", - "types": "./lib/types/types/index.d.ts", + "types": "./lib/types/src/types/index.d.ts", "exports": { ".": { - "require": "./lib/cjs/types/index.js", - "import": "./lib/esm/types/index.js", - "types": "./lib/types/types/index.d.ts", - "default": "./lib/esm/types/index.js" + "require": "./lib/cjs/src/types/index.js", + "import": "./lib/esm/src/types/index.js", + "types": "./lib/types/src/types/index.d.ts", + "default": "./lib/esm/src/types/index.js" }, "./eval": { - "types": "./lib/types/eval/index.d.ts", - "require": "./lib/cjs/eval/index.js", - "import": "./lib/esm/eval/index.js", - "default": "./lib/esm/eval/index.js" + "types": "./lib/types/src/eval/index.d.ts", + "require": "./lib/cjs/src/eval/index.js", + "import": "./lib/esm/src/eval/index.js", + "default": "./lib/esm/src/eval/index.js" }, "./plugin": { - "types": "./lib/types/plugin/index.d.ts", - "require": "./lib/cjs/plugin/index.js", - "import": "./lib/esm/plugin/index.js", - "default": "./lib/esm/plugin/index.js" + "types": "./lib/types/src/plugin/index.d.ts", + "require": "./lib/cjs/src/plugin/index.js", + "import": "./lib/esm/src/plugin/index.js", + "default": "./lib/esm/src/plugin/index.js" }, "./manager": { - "types": "./lib/manager/index.d.ts", - "require": "./lib/cjs/manager/index.js", - "import": "./lib/esm/manager/index.js", - "default": "./lib/esm/manager/index.js" + "types": "./lib/types/src/manager/index.d.ts", + "require": "./lib/cjs/src/manager/index.js", + "import": "./lib/esm/src/manager/index.js", + "default": "./lib/esm/src/manager/index.js" }, "./server": { - "types": "./lib/server/index.d.ts", - "require": "./lib/cjs/server/index.js", - "import": "./lib/esm/server/index.js", - "default": "./lib/esm/server/index.js" + "types": "./lib/types/src/server/index.d.ts", + "require": "./lib/cjs/src/server/index.js", + "import": "./lib/esm/src/server/index.js", + "default": "./lib/esm/src/server/index.js" }, "./utils": { - "types": "./lib/utils/index.d.ts", - "require": "./lib/cjs/utils/index.js", - "import": "./lib/esm/utils/index.js", - "default": "./lib/esm/utils/index.js" + "types": "./lib/types/src/utils/index.d.ts", + "require": "./lib/cjs/src/utils/index.js", + "import": "./lib/esm/src/utils/index.js", + "default": "./lib/esm/src/utils/index.js" } }, "typesVersions": { "*": { "eval": [ - "lib/types/eval" + "lib/types/src/eval" ], "plugin": [ - "lib/types/plugin" + "lib/types/src/plugin" ], "manager": [ - "lib/types/manager" + "lib/types/src/manager" ], "server": [ - "lib/types/server" + "lib/types/src/server" ], "utils": [ - "lib/types/utils" + "lib/types/src/utils" ] } } diff --git a/genkit-tools/common/src/utils/package.ts b/genkit-tools/common/src/utils/package.ts index 7e94aef73..88561673d 100644 --- a/genkit-tools/common/src/utils/package.ts +++ b/genkit-tools/common/src/utils/package.ts @@ -14,8 +14,6 @@ * limitations under the License. */ -import { readFileSync } from 'fs'; -import { join } from 'path'; +import toolsPackage from '../../package.json'; -const packagePath = join(__dirname, '../../../package.json'); -export const toolsPackage = JSON.parse(readFileSync(packagePath, 'utf8')); +export { toolsPackage }; diff --git a/genkit-tools/common/tsconfig.json b/genkit-tools/common/tsconfig.json index b63e7bcd2..035ea1b99 100644 --- a/genkit-tools/common/tsconfig.json +++ b/genkit-tools/common/tsconfig.json @@ -6,7 +6,8 @@ "outDir": "lib/esm", "esModuleInterop": true, "typeRoots": ["./node_modules/@types"], - "rootDirs": ["src"] + "rootDirs": ["src"], + "resolveJsonModule": true }, "include": ["src"] } From d309eb8f2a5a672ad053219180138aae005c760f Mon Sep 17 00:00:00 2001 From: Jacob Cable Date: Wed, 18 Jun 2025 14:43:19 +0100 Subject: [PATCH 05/21] ci: add build-cli-binaries --- .github/workflows/build-cli-binaries.yml | 97 ++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 .github/workflows/build-cli-binaries.yml diff --git a/.github/workflows/build-cli-binaries.yml b/.github/workflows/build-cli-binaries.yml new file mode 100644 index 000000000..2613133c9 --- /dev/null +++ b/.github/workflows/build-cli-binaries.yml @@ -0,0 +1,97 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +name: Build CLI Binaries + +on: + workflow_dispatch: + +jobs: + build: + strategy: + matrix: + include: + - os: ubuntu-latest + target: linux-x64 + - os: macos-latest + target: darwin-x64 + - os: macos-13 + target: darwin-x64-intel + - os: windows-latest + target: win32-x64 + + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Setup pnpm + uses: pnpm/action-setup@v3 + with: + version: 10.11.0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build workspace packages + run: | + cd genkit-tools + pnpm build:common + pnpm build:telemetry-server + pnpm build:cli + + - name: Set binary extension + id: binary + shell: bash + run: | + if [[ "${{ matrix.target }}" == win32-* ]]; then + echo "ext=.exe" >> $GITHUB_OUTPUT + else + echo "ext=" >> $GITHUB_OUTPUT + fi + + - name: Compile binary + run: | + cd genkit-tools/cli + bun build src/bin/genkit.ts \ + --compile \ + --target=bun \ + --outfile dist/bin/genkit-${{ matrix.target }}${{ steps.binary.outputs.ext }} + + - name: Test binary + shell: bash + run: | + cd genkit-tools/cli + ./dist/bin/genkit-${{ matrix.target }}${{ steps.binary.outputs.ext }} --version + + - name: Upload binary artifact + uses: actions/upload-artifact@v4 + with: + name: genkit-${{ matrix.target }} + path: genkit-tools/cli/dist/bin/genkit-${{ matrix.target }}${{ steps.binary.outputs.ext }} + retention-days: 1 \ No newline at end of file From 70faea574d43f4a2d99938f6ddd9fb25bdeb73c3 Mon Sep 17 00:00:00 2001 From: Jacob Cable <32874567+cabljac@users.noreply.github.com> Date: Wed, 18 Jun 2025 14:48:23 +0100 Subject: [PATCH 06/21] Update js/testapps/basic-gemini/src/index.ts --- js/testapps/basic-gemini/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/testapps/basic-gemini/src/index.ts b/js/testapps/basic-gemini/src/index.ts index df69be93d..fa2882d8e 100644 --- a/js/testapps/basic-gemini/src/index.ts +++ b/js/testapps/basic-gemini/src/index.ts @@ -40,7 +40,7 @@ export const jokeFlow = ai.defineFlow( prompt: `Tell me a really funny joke about ${subject}`, }); if (!llmResponse.output) { - throw new Error('oh no!'); + throw new Error('Failed to generate a response from the AI model. Please check the model configuration and input data.'); } return llmResponse.output!; } From d0e5ad2559469a2c07db769df74deebbbbfc1d82 Mon Sep 17 00:00:00 2001 From: Jacob Cable Date: Wed, 18 Jun 2025 14:52:08 +0100 Subject: [PATCH 07/21] chore(testapps): revert basic-gemini testapp --- js/testapps/basic-gemini/src/index.ts | 30 ++++++++++++++++++--------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/js/testapps/basic-gemini/src/index.ts b/js/testapps/basic-gemini/src/index.ts index fa2882d8e..90ae4da62 100644 --- a/js/testapps/basic-gemini/src/index.ts +++ b/js/testapps/basic-gemini/src/index.ts @@ -22,26 +22,36 @@ const ai = genkit({ plugins: [googleAI(), vertexAI()], }); +const jokeSubjectGenerator = ai.defineTool( + { + name: 'jokeSubjectGenerator', + description: 'Can be called to generate a subject for a joke', + }, + async () => { + return 'banana'; + } +); + export const jokeFlow = ai.defineFlow( { name: 'jokeFlow', - inputSchema: z.object({ subject: z.string() }), - outputSchema: z.object({ joke: z.string() }), + inputSchema: z.void(), + outputSchema: z.any(), }, - async ({ subject }) => { + async () => { const llmResponse = await ai.generate({ model: gemini15Flash, config: { - temperature: 0.7, + temperature: 2, + // if desired, model versions can be explicitly set + version: 'gemini-1.5-flash-002', }, output: { - schema: z.object({ joke: z.string() }), + schema: z.object({ jokeSubject: z.string() }), }, - prompt: `Tell me a really funny joke about ${subject}`, + tools: [jokeSubjectGenerator], + prompt: `come up with a subject to joke about (using the function provided)`, }); - if (!llmResponse.output) { - throw new Error('Failed to generate a response from the AI model. Please check the model configuration and input data.'); - } - return llmResponse.output!; + return llmResponse.output; } ); From 878c6cf235e39049eb0dc3691dcb7b322698a81d Mon Sep 17 00:00:00 2001 From: Jacob Cable Date: Wed, 18 Jun 2025 14:59:32 +0100 Subject: [PATCH 08/21] ci: change trigger for testing on github --- .github/workflows/build-cli-binaries.yml | 48 +++++++----------------- 1 file changed, 13 insertions(+), 35 deletions(-) diff --git a/.github/workflows/build-cli-binaries.yml b/.github/workflows/build-cli-binaries.yml index 2613133c9..4264b3285 100644 --- a/.github/workflows/build-cli-binaries.yml +++ b/.github/workflows/build-cli-binaries.yml @@ -1,22 +1,9 @@ -# Copyright 2025 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# SPDX-License-Identifier: Apache-2.0 - name: Build CLI Binaries on: + push: + branches: + - '@invertase/cli-binary' workflow_dispatch: jobs: @@ -55,15 +42,14 @@ jobs: node-version: '20' cache: 'pnpm' - - name: Install dependencies - run: pnpm install --frozen-lockfile + - name: Install root dependencies + run: pnpm i - - name: Build workspace packages - run: | - cd genkit-tools - pnpm build:common - pnpm build:telemetry-server - pnpm build:cli + - name: Install genkit-tools dependencies + run: pnpm pnpm-install-genkit-tools + + - name: Build genkit-tools + run: pnpm build:genkit-tools - name: Set binary extension id: binary @@ -75,19 +61,11 @@ jobs: echo "ext=" >> $GITHUB_OUTPUT fi - - name: Compile binary - run: | - cd genkit-tools/cli - bun build src/bin/genkit.ts \ - --compile \ - --target=bun \ - --outfile dist/bin/genkit-${{ matrix.target }}${{ steps.binary.outputs.ext }} - - - name: Test binary - shell: bash + - name: Compile CLI binary run: | cd genkit-tools/cli - ./dist/bin/genkit-${{ matrix.target }}${{ steps.binary.outputs.ext }} --version + pnpm compile:bun + mv dist/bin/genkit dist/bin/genkit-${{ matrix.target }}${{ steps.binary.outputs.ext }} - name: Upload binary artifact uses: actions/upload-artifact@v4 From 9e8229c5f5237d2dff81093119d386858376a4f5 Mon Sep 17 00:00:00 2001 From: Jacob Cable Date: Wed, 18 Jun 2025 16:30:07 +0100 Subject: [PATCH 09/21] feat: add first draft of CLI install script --- .github/workflows/build-cli-binaries.yml | 16 ++ bin/install_cli | 349 +++++++++++++++++++++++ 2 files changed, 365 insertions(+) create mode 100644 bin/install_cli diff --git a/.github/workflows/build-cli-binaries.yml b/.github/workflows/build-cli-binaries.yml index 4264b3285..f48674707 100644 --- a/.github/workflows/build-cli-binaries.yml +++ b/.github/workflows/build-cli-binaries.yml @@ -1,3 +1,19 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 + name: Build CLI Binaries on: diff --git a/bin/install_cli b/bin/install_cli new file mode 100644 index 000000000..af2c6bdc6 --- /dev/null +++ b/bin/install_cli @@ -0,0 +1,349 @@ +#!/usr/bin/env bash +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 + + +## +## +## +## +## + +# Configuration variables +DOMAIN="genkit.tools" +TRACKING_ID="UA-XXXXXXXXX-X" # Not used when analytics is commented out + +: ========================================== +: Introduction +: ========================================== + +# This script allows you to install the latest version of the +# "genkit" command by running: +# +: curl -sL $DOMAIN | bash +# +# If you do not want to use this script, you can manually +# download the latest "genkit" binary. +# +: curl -Lo ./genkit_bin https://$DOMAIN/bin/linux/latest +# +# Alternatively, you can download a specific version. +# +: curl -Lo ./genkit_bin https://$DOMAIN/bin/linux/v1.12.0 +# +# Note: On Mac, replace "linux" with "macos" in the URL. +# +# For full details about installation options for the Genkit CLI +# please see our documentation. +# https://firebase.google.com/docs/genkit/ +# +# Please report bugs / issues with this script on Github. +# https://github.com/firebase/genkit +# + +: ========================================== +: Advanced Usage +: ========================================== + +# The behavior of this script can be modified at runtime by passing environmental +# variables to the `bash` process. +# +# For example, passing an argument called arg1 set to true and one called arg2 set +# to false would look like this. +# +: curl -sL $DOMAIN | arg1=true arg2=false bash +# +# These arguments are optional, but be aware that explicitly setting them will help +# ensure consistent behavior if / when defaults are changed. +# + +: ----------------------------------------- +: Upgrading - default: false +: ----------------------------------------- + +# By default, this script will not replace an existing "genkit" install. +# If you'd like to upgrade an existing install, set the "upgrade" variable to true. +# +: curl -sL $DOMAIN | upgrade=true bash +# +# This operation could (potentially) break an existing install, so use it with caution. +# + +: ----------------------------------------- +: Uninstalling - default false +: ----------------------------------------- + +# You can remove the binary by passing the "uninstall" flag. +# +: curl -sL $DOMAIN | uninstall=true bash +# +# This will remove the binary file and any cached data. +# + +: ----------------------------------------- +: Analytics - default true +: ----------------------------------------- + +# This script reports anonymous success / failure analytics. +# You can disable this reporting by setting the "analytics" variable to false. +# +: curl -sL $DOMAIN | analytics=false bash +# +# By default we report all data anonymously and do not collect any information +# except platform type (Darwin, Win, etc) in the case of an unsupported platform +# error. +# + +: ========================================== +: Source Code +: ========================================== + +# This script contains a large amount of comments so you can understand +# how it interacts with your system. If you're not interested in the +# technical details, you can just run the command above. + +# We begin by generating a unique ID for tracking the anonymous session. +CID=$(head -80 /dev/urandom | LC_ALL=c tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1) +# Credit: https://gist.github.com/earthgecko/3089509 + +# We can use this CID in all calls to the Google Analytics endpoint via +# this reusable function. +send_analytics_event() +{ + # Analytics tracking is currently disabled + # Uncomment the block below to enable analytics + + # if [ ! "$analytics" = "false" ]; then + # curl -s https://www.google-analytics.com/collect \ + # -d "tid=$TRACKING_ID" \ + # -d "t=event" \ + # -d "ec=$DOMAIN" \ + # -d "ea=$1" \ + # -d "v=1" \ + # -d "cid=$CID" \ + # -o /dev/null + # fi + + # For now, just return success + return 0 +} + +# We send one event to count the number of times this script is ran. At the +# end we also report success / failure, but it's possible that the script +# will crash before we get to that point, so we manually count invocations here. +send_analytics_event start + +# We try to detect any existing binaries on $PATH or two common locations. +GENKIT_BINARY=${GENKIT_BINARY:-$(which genkit)} +LOCAL_BINARY="$HOME/.local/bin/genkit" +# For info about why we place the binary at this location, see +# https://unix.stackexchange.com/a/8658 +GLOBAL_BINARY="/usr/local/bin/genkit" +if [[ -z "$GENKIT_BINARY" ]]; then + if [ -e "$LOCAL_BINARY" ]; then + GENKIT_BINARY="$LOCAL_BINARY" + elif [ -e "$GLOBAL_BINARY" ]; then + GENKIT_BINARY="$GLOBAL_BINARY" + fi +fi + +# If the user asked for us to uninstall genkit, then do so. +if [ "$uninstall" = "true" ]; then + if [[ -z "$GENKIT_BINARY" ]]; then + echo "Cannot detect any Genkit CLI installations." + echo "Please manually remove any \"genkit\" binaries not in \$PATH." + else + # Assuming binary install, skip npm check + echo "-- Removing binary file..." + sudo rm -- "$GENKIT_BINARY" + fi + echo "-- Removing genkit cache..." + rm -rf ~/.cache/genkit + + echo "-- genkit has been uninstalled" + echo "-- All Done!" + + send_analytics_event uninstall + exit 0 +fi + +# We need to ensure that we don't mess up an existing "genkit" +# install, so before doing anything we check to see if this system +# has "genkit" installed and if so, we exit out. +echo "-- Checking for existing genkit installation..." + +if [[ ! -z "$GENKIT_BINARY" ]]; then + INSTALLED_GENKIT_VERSION=$("$GENKIT_BINARY" --version) + + # In the case of a corrupt genkit install, we wont be able to + # retrieve a version number, so to keep the logs correct, we refer to + # your existing install as either the CLI version or as a "corrupt install" + if [[ ! -z "$INSTALLED_GENKIT_VERSION" ]]; then + GENKIT_NICKNAME="genkit@$INSTALLED_GENKIT_VERSION" + else + GENKIT_NICKNAME="a corrupted genkit binary" + fi + + # Skip npm check - assume binary install + # If the user didn't pass upgrade=true, then we print the command to do an upgrade and exit + if [ ! "$upgrade" = "true" ]; then + echo "Your machine has $GENKIT_NICKNAME installed." + echo "If you would like to upgrade your install run: curl -sL $DOMAIN | upgrade=true bash" + + send_analytics_event already_installed + exit 0 + else + # If the user did pass upgrade=true, then we allow the script to continue and overwrite the install. + echo "-- Your machine has $GENKIT_NICKNAME, attempting upgrade..." + + send_analytics_event upgrade + fi +fi + +echo "-- Checking your machine type..." + +# Now we need to detect the platform we're running on (Linux / Mac / Other) +# so we can fetch the correct binary and place it in the correct location +# on the machine. + +# We use "tr" to translate the uppercase "uname" output into lowercase +UNAME=$(uname -s | tr '[:upper:]' '[:lower:]') + +# Detect architecture +ARCH=$(uname -m) +case "$ARCH" in + x86_64) ARCH_SUFFIX="x64";; + aarch64|arm64) ARCH_SUFFIX="arm64";; + *) ARCH_SUFFIX="x64";; # Default to x64 +esac + +# Then we map the output to the names used on the Github releases page +case "$UNAME" in + linux*) MACHINE="linux-${ARCH_SUFFIX}";; + darwin*) MACHINE="darwin-${ARCH_SUFFIX}";; + mingw*|msys*|cygwin*) MACHINE="win32-x64";; +esac + +# If we never define the $MACHINE variable (because our platform is neither Mac, +# Linux, or Windows), then we can't finish our job, so just log out a helpful message +# and close. +if [[ -z "$MACHINE" ]]; then + echo "Your operating system is not supported, if you think it should be please file a bug." + echo "https://github.com/firebase/genkit/" + echo "-- All done!" + + send_analytics_event "missing_platform_${UNAME}_${ARCH}" + exit 0 +fi + +# We have enough information to generate the binary's download URL. +DOWNLOAD_URL="https://$DOMAIN/bin/$MACHINE/latest" +echo "-- Downloading binary from $DOWNLOAD_URL" + +# We use "curl" to download the binary with a flag set to follow redirects +# (Github download URLs redirect to CDNs) and a flag to show a progress bar. +curl -o "/tmp/genkit_standalone.tmp" -L --progress-bar $DOWNLOAD_URL + +GENKIT_BINARY=${GENKIT_BINARY:-$GLOBAL_BINARY} +INSTALL_DIR=$(dirname -- "$GENKIT_BINARY") + +# We need to ensure that the INSTALL_DIR exists. +# On some platforms like the Windows Subsystem for Linux it may not. +# We created it using a non-destructive mkdir command. +mkdir -p -- "$INSTALL_DIR" 2> /dev/null + +# If the directory does not exist or is not writable, we resort to sudo. +sudo="" +if [ ! -w "$INSTALL_DIR" ]; then + sudo="sudo" +fi + +$sudo mkdir -p -- "$INSTALL_DIR" +$sudo mv -f "/tmp/genkit_standalone.tmp" "$GENKIT_BINARY" + +# Once the download is complete, we mark the binary file as readable +# and executable (+rx). +echo "-- Setting permissions on binary... $GENKIT_BINARY" +$sudo chmod +rx "$GENKIT_BINARY" + +# If all went well, the "genkit" binary should be located on our PATH so +# we'll run it once, asking it to print out the version. This is helpful as +# standalone genkit binaries do a small amount of setup on the initial run +# so this not only allows us to make sure we got the right version, but it +# also does the setup so the first time the developer runs the binary, it'll +# be faster. +VERSION=$("$GENKIT_BINARY" --version) + +# If no version is detected then clearly the binary failed to install for +# some reason, so we'll log out an error message and report the failure +# to headquarters via an analytics event. +if [[ -z "$VERSION" ]]; then + echo "Something went wrong, genkit has not been installed." + echo "Please file a bug with your system information on Github." + echo "https://github.com/firebase/genkit/" + echo "-- All done!" + + send_analytics_event failure + exit 1 +fi + +# In order for the user to be able to actually run the "genkit" command +# without specifying the absolute location, the INSTALL_DIR path must +# be present inside of the PATH environment variable. + +echo "-- Checking your PATH variable..." +if [[ ! ":$PATH:" == *":$INSTALL_DIR:"* ]]; then + echo "" + echo "It looks like $INSTALL_DIR isn't on your PATH." + echo "Please add the following line to either your ~/.profile or ~/.bash_profile, then restart your terminal." + echo "" + echo "PATH=\$PATH:$INSTALL_DIR" + echo "" + echo "For more information about modifying PATHs, see https://unix.stackexchange.com/a/26059" + echo "" + send_analytics_event missing_path +fi + +# We also try to upgrade the local binary if it exists. +# This helps prevent having two mismatching versions of "genkit". +if [[ "$GENKIT_BINARY" != "$LOCAL_BINARY" ]] && [ -e "$LOCAL_BINARY" ]; then + echo "-- Upgrading the local binary installation $LOCAL_BINARY..." + cp "$GENKIT_BINARY" "$LOCAL_BINARY" # best effort, okay if it fails. + chmod +x "$LOCAL_BINARY" +fi + +# Since we've gotten this far we know everything succeeded. We'll just +# let the developer know everything is ready and take our leave. +echo "-- genkit@$VERSION is now installed" +echo "-- All Done!" + +send_analytics_event success +exit 0 + +# ------------------------------------------ +# Notes +# ------------------------------------------ +# +# This script contains hidden JavaScript which is used to improve +# readability in the browser (via syntax highlighting, etc), right-click +# and "View source" of this page to see the entire bash script! +# +# You'll also notice that we use the ":" character in the Introduction +# which allows our copy/paste commands to be syntax highlighted, but not +# ran. In bash : is equal to `true` and true can take infinite arguments +# while still returning true. This turns these commands into no-ops so +# when ran as a script, they're totally ignored. +# \ No newline at end of file From f55ca106a997a23e711294253896d596756c2818 Mon Sep 17 00:00:00 2001 From: Jacob Cable Date: Fri, 20 Jun 2025 10:04:43 +0100 Subject: [PATCH 10/21] fix(ci): adjust file extension for windows --- .github/workflows/build-cli-binaries.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-cli-binaries.yml b/.github/workflows/build-cli-binaries.yml index f48674707..2ddb285ca 100644 --- a/.github/workflows/build-cli-binaries.yml +++ b/.github/workflows/build-cli-binaries.yml @@ -78,10 +78,19 @@ jobs: fi - name: Compile CLI binary + shell: bash run: | cd genkit-tools/cli pnpm compile:bun - mv dist/bin/genkit dist/bin/genkit-${{ matrix.target }}${{ steps.binary.outputs.ext }} + + # Handle the binary name based on OS + if [[ "${{ matrix.os }}" == windows-* ]]; then + # On Windows, Bun outputs genkit.exe + mv dist/bin/genkit.exe "dist/bin/genkit-${{ matrix.target }}.exe" + else + # On Unix-like systems, no extension + mv dist/bin/genkit "dist/bin/genkit-${{ matrix.target }}" + fi - name: Upload binary artifact uses: actions/upload-artifact@v4 From ea2293893e4eae65d7333498be94e9fde493b9b9 Mon Sep 17 00:00:00 2001 From: Jacob Cable Date: Fri, 20 Jun 2025 10:07:13 +0100 Subject: [PATCH 11/21] feat(ci): add arm runners --- .github/workflows/build-cli-binaries.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/build-cli-binaries.yml b/.github/workflows/build-cli-binaries.yml index 2ddb285ca..c7246213d 100644 --- a/.github/workflows/build-cli-binaries.yml +++ b/.github/workflows/build-cli-binaries.yml @@ -29,12 +29,16 @@ jobs: include: - os: ubuntu-latest target: linux-x64 + - os: ubuntu-24.04-arm + target: linux-arm64 - os: macos-latest target: darwin-x64 - os: macos-13 target: darwin-x64-intel - os: windows-latest target: win32-x64 + - os: windows-11-arm + target: win32-arm64 runs-on: ${{ matrix.os }} From 44247ff9ae233fcb1802f3c7b953a40964543223 Mon Sep 17 00:00:00 2001 From: Jacob Cable Date: Fri, 20 Jun 2025 11:46:50 +0100 Subject: [PATCH 12/21] feat(ci): add tests and update yaml --- .github/workflows/build-cli-binaries.yml | 97 ++++++++++++++++++++++-- 1 file changed, 91 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-cli-binaries.yml b/.github/workflows/build-cli-binaries.yml index c7246213d..1c477c6b5 100644 --- a/.github/workflows/build-cli-binaries.yml +++ b/.github/workflows/build-cli-binaries.yml @@ -31,14 +31,17 @@ jobs: target: linux-x64 - os: ubuntu-24.04-arm target: linux-arm64 - - os: macos-latest + - os: macos-13 # x64/Intel target: darwin-x64 - - os: macos-13 - target: darwin-x64-intel + - os: macos-latest # ARM64/M1 + target: darwin-arm64 - os: windows-latest target: win32-x64 - - os: windows-11-arm - target: win32-arm64 + # Note: Windows ARM64 currently runs x64 binaries through emulation + # Native ARM64 support is not yet available in Bun + # See: https://github.com/oven-sh/bun/pull/11430 + # - os: windows-11-arm + # target: win32-arm64 runs-on: ${{ matrix.os }} @@ -101,4 +104,86 @@ jobs: with: name: genkit-${{ matrix.target }} path: genkit-tools/cli/dist/bin/genkit-${{ matrix.target }}${{ steps.binary.outputs.ext }} - retention-days: 1 \ No newline at end of file + retention-days: 1 + + test: + needs: build + strategy: + matrix: + include: + - os: ubuntu-latest + target: linux-x64 + - os: ubuntu-24.04-arm + target: linux-arm64 + - os: macos-13 + target: darwin-x64 + - os: macos-latest + target: darwin-arm64 + - os: windows-latest + target: win32-x64 + + runs-on: ${{ matrix.os }} + + steps: + - name: Set binary extension + id: binary + shell: bash + run: | + if [[ "${{ matrix.target }}" == win32-* ]]; then + echo "ext=.exe" >> $GITHUB_OUTPUT + else + echo "ext=" >> $GITHUB_OUTPUT + fi + + - name: Download binary artifact + uses: actions/download-artifact@v4 + with: + name: genkit-${{ matrix.target }} + path: ./ + + - name: Make binary executable (Unix) + if: runner.os != 'Windows' + run: chmod +x genkit-${{ matrix.target }} + + - name: Test --help command + shell: bash + run: | + echo "Testing genkit --help" + ./genkit-${{ matrix.target }}${{ steps.binary.outputs.ext }} --help + + - name: Test --version command + shell: bash + run: | + echo "Testing genkit --version" + ./genkit-${{ matrix.target }}${{ steps.binary.outputs.ext }} --version + + - name: Verify UI commands exist + shell: bash + run: | + echo "Verifying UI commands are available" + ./genkit-${{ matrix.target }}${{ steps.binary.outputs.ext }} ui:start --help + ./genkit-${{ matrix.target }}${{ steps.binary.outputs.ext }} ui:stop --help + + - name: Test basic UI functionality (Unix only) + if: runner.os != 'Windows' + shell: bash + run: | + echo "Testing genkit ui:start" + # Start the UI in the background + ./genkit-${{ matrix.target }} ui:start & + UI_PID=$! + + # Give it some time to start + sleep 5 + + # Check if the process is still running + if ps -p $UI_PID > /dev/null 2>&1; then + echo "UI process started successfully (PID: $UI_PID)" + # Clean up - kill the process + kill $UI_PID 2>/dev/null || true + sleep 2 + echo "UI process terminated" + else + echo "UI process failed to start or exited immediately" + # This might be expected without a proper project, so we don't fail + fi \ No newline at end of file From 91505bf8cf03c582dc7a1a38b57a77bbc742ecea Mon Sep 17 00:00:00 2001 From: Jacob Cable Date: Fri, 20 Jun 2025 12:23:03 +0100 Subject: [PATCH 13/21] fix(ci): update ci workflow unix tests --- .github/workflows/build-cli-binaries.yml | 42 ++++++++++++++++-------- 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/.github/workflows/build-cli-binaries.yml b/.github/workflows/build-cli-binaries.yml index 1c477c6b5..95776d3cb 100644 --- a/.github/workflows/build-cli-binaries.yml +++ b/.github/workflows/build-cli-binaries.yml @@ -164,26 +164,42 @@ jobs: ./genkit-${{ matrix.target }}${{ steps.binary.outputs.ext }} ui:start --help ./genkit-${{ matrix.target }}${{ steps.binary.outputs.ext }} ui:stop --help - - name: Test basic UI functionality (Unix only) + - name: Test UI start functionality (Unix only) if: runner.os != 'Windows' shell: bash run: | echo "Testing genkit ui:start" - # Start the UI in the background - ./genkit-${{ matrix.target }} ui:start & + + # Start UI in background, piping any prompts to accept them + (echo "" | ./genkit-${{ matrix.target }} ui:start 2>&1 | tee ui_output.log) & UI_PID=$! - # Give it some time to start + # Give it time to start sleep 5 - # Check if the process is still running - if ps -p $UI_PID > /dev/null 2>&1; then - echo "UI process started successfully (PID: $UI_PID)" - # Clean up - kill the process - kill $UI_PID 2>/dev/null || true + # Check if it started successfully by looking for the expected output + if grep -q "Genkit Developer UI started at:" ui_output.log 2>/dev/null; then + echo "✓ UI started successfully" + cat ui_output.log + + # Try to stop it gracefully + echo "Testing genkit ui:stop" + ./genkit-${{ matrix.target }} ui:stop || true + + # Give it time to stop sleep 2 - echo "UI process terminated" else - echo "UI process failed to start or exited immediately" - # This might be expected without a proper project, so we don't fail - fi \ No newline at end of file + echo "UI output:" + cat ui_output.log 2>/dev/null || echo "No output captured" + + # Check if process is still running + if ps -p $UI_PID > /dev/null 2>&1; then + echo "Process is running but didn't produce expected output" + kill $UI_PID 2>/dev/null || true + else + echo "Process exited (might be due to cookie prompt or missing project)" + fi + fi + + # Clean up any remaining processes + pkill -f "genkit.*ui:start" 2>/dev/null || true \ No newline at end of file From 8394cedba3519d17f4c3290608f9812829b04c2e Mon Sep 17 00:00:00 2001 From: Jacob Cable Date: Fri, 20 Jun 2025 12:44:41 +0100 Subject: [PATCH 14/21] feat(ci): add windows binary testing --- .github/workflows/build-cli-binaries.yml | 58 +++++++++++++++++++++++- 1 file changed, 56 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-cli-binaries.yml b/.github/workflows/build-cli-binaries.yml index 95776d3cb..3b1680935 100644 --- a/.github/workflows/build-cli-binaries.yml +++ b/.github/workflows/build-cli-binaries.yml @@ -104,7 +104,7 @@ jobs: with: name: genkit-${{ matrix.target }} path: genkit-tools/cli/dist/bin/genkit-${{ matrix.target }}${{ steps.binary.outputs.ext }} - retention-days: 1 + retention-days: 1 # TODO: Consider increasing to 7 days for better debugging capability test: needs: build @@ -202,4 +202,58 @@ jobs: fi # Clean up any remaining processes - pkill -f "genkit.*ui:start" 2>/dev/null || true \ No newline at end of file + pkill -f "genkit.*ui:start" 2>/dev/null || true + + - name: Test UI start functionality (Windows only) + if: runner.os == 'Windows' + shell: pwsh + run: | + Write-Host "Testing genkit ui:start" + + # Create empty input file first for redirecting stdin + "" | Out-File -FilePath ".\empty.txt" + + # Start UI in background, redirecting input to handle prompts + $process = Start-Process -FilePath ".\genkit-${{ matrix.target }}.exe" ` + -ArgumentList "ui:start" ` + -RedirectStandardInput ".\empty.txt" ` + -RedirectStandardOutput ".\ui_output.log" ` + -RedirectStandardError ".\ui_error.log" ` + -PassThru ` + -NoNewWindow + + # Give it time to start + Start-Sleep -Seconds 5 + + # Read the output + $output = Get-Content ".\ui_output.log" -ErrorAction SilentlyContinue + $errorOutput = Get-Content ".\ui_error.log" -ErrorAction SilentlyContinue + + if ($output -match "Genkit Developer UI started at:") { + Write-Host "✓ UI started successfully" + Write-Host "Output:" + $output | Write-Host + + # Try to stop it gracefully + Write-Host "Testing genkit ui:stop" + & ".\genkit-${{ matrix.target }}.exe" ui:stop + + Start-Sleep -Seconds 2 + } else { + Write-Host "UI did not start as expected" + Write-Host "Output:" + $output | Write-Host + Write-Host "Error:" + $errorOutput | Write-Host + + # Check if process is still running + if (-not $process.HasExited) { + Write-Host "Process is still running, terminating..." + Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue + } else { + Write-Host "Process exited (might be due to cookie prompt or missing project)" + } + } + + # Clean up any remaining genkit processes + Get-Process | Where-Object { $_.ProcessName -match "genkit" } | Stop-Process -Force -ErrorAction SilentlyContinue \ No newline at end of file From 00591eb650c52464926c4e9874bd49ec4d553f62 Mon Sep 17 00:00:00 2001 From: Jacob Cable Date: Mon, 23 Jun 2025 10:44:19 +0100 Subject: [PATCH 15/21] refactor: move server-harness to command file --- genkit-tools/cli/src/cli.ts | 2 +- .../{utils/server-harness.ts => commands/ui-start-server.ts} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename genkit-tools/cli/src/{utils/server-harness.ts => commands/ui-start-server.ts} (97%) diff --git a/genkit-tools/cli/src/cli.ts b/genkit-tools/cli/src/cli.ts index ef071c5e4..54f638040 100644 --- a/genkit-tools/cli/src/cli.ts +++ b/genkit-tools/cli/src/cli.ts @@ -31,8 +31,8 @@ import { flowRun } from './commands/flow-run'; import { getPluginCommands, getPluginSubCommand } from './commands/plugins'; import { start } from './commands/start'; import { uiStart } from './commands/ui-start'; +import { uiStartServer } from './commands/ui-start-server'; import { uiStop } from './commands/ui-stop'; -import { uiStartServer } from './utils/server-harness'; import { version } from './utils/version'; /** diff --git a/genkit-tools/cli/src/utils/server-harness.ts b/genkit-tools/cli/src/commands/ui-start-server.ts similarity index 97% rename from genkit-tools/cli/src/utils/server-harness.ts rename to genkit-tools/cli/src/commands/ui-start-server.ts index b78db09db..ab4e86a62 100644 --- a/genkit-tools/cli/src/utils/server-harness.ts +++ b/genkit-tools/cli/src/commands/ui-start-server.ts @@ -17,7 +17,7 @@ import { startServer } from '@genkit-ai/tools-common/server'; import { Command } from 'commander'; import fs from 'fs'; -import { startManager } from './manager-utils'; +import { startManager } from '../utils/manager-utils'; function redirectStdoutToFile(logFile: string) { const myLogFileStream = fs.createWriteStream(logFile); From 614c5a164f05fe0cbb2f6dec38ae77e1042f1c31 Mon Sep 17 00:00:00 2001 From: Jacob Cable <32874567+cabljac@users.noreply.github.com> Date: Tue, 24 Jun 2025 12:22:53 +0100 Subject: [PATCH 16/21] Update bin/install_cli Co-authored-by: Elliot Hesp --- bin/install_cli | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/install_cli b/bin/install_cli index af2c6bdc6..4e2422797 100644 --- a/bin/install_cli +++ b/bin/install_cli @@ -292,7 +292,7 @@ VERSION=$("$GENKIT_BINARY" --version) # to headquarters via an analytics event. if [[ -z "$VERSION" ]]; then echo "Something went wrong, genkit has not been installed." - echo "Please file a bug with your system information on Github." + echo "Please file a bug with your system information on GitHub." echo "https://github.com/firebase/genkit/" echo "-- All done!" From 093397342e3ad7a067192582f0fe3bda37f19804 Mon Sep 17 00:00:00 2001 From: Jacob Cable <32874567+cabljac@users.noreply.github.com> Date: Tue, 24 Jun 2025 12:23:05 +0100 Subject: [PATCH 17/21] Update bin/install_cli Co-authored-by: Elliot Hesp --- bin/install_cli | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/install_cli b/bin/install_cli index 4e2422797..51bd72a78 100644 --- a/bin/install_cli +++ b/bin/install_cli @@ -50,7 +50,7 @@ TRACKING_ID="UA-XXXXXXXXX-X" # Not used when analytics is commented out # please see our documentation. # https://firebase.google.com/docs/genkit/ # -# Please report bugs / issues with this script on Github. +# Please report bugs / issues with this script on GitHub. # https://github.com/firebase/genkit # From f35b2ccf2f7eaa8159325aed96ce13c3c9142985 Mon Sep 17 00:00:00 2001 From: Jacob Cable <32874567+cabljac@users.noreply.github.com> Date: Tue, 24 Jun 2025 12:23:59 +0100 Subject: [PATCH 18/21] Update bin/install_cli Co-authored-by: Elliot Hesp --- bin/install_cli | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/install_cli b/bin/install_cli index 51bd72a78..963ec0b16 100644 --- a/bin/install_cli +++ b/bin/install_cli @@ -254,7 +254,7 @@ DOWNLOAD_URL="https://$DOMAIN/bin/$MACHINE/latest" echo "-- Downloading binary from $DOWNLOAD_URL" # We use "curl" to download the binary with a flag set to follow redirects -# (Github download URLs redirect to CDNs) and a flag to show a progress bar. +# (GitHub download URLs redirect to CDNs) and a flag to show a progress bar. curl -o "/tmp/genkit_standalone.tmp" -L --progress-bar $DOWNLOAD_URL GENKIT_BINARY=${GENKIT_BINARY:-$GLOBAL_BINARY} From 39b62a04ffcae14f97da00dc1c0b469bd5bba86d Mon Sep 17 00:00:00 2001 From: Jacob Cable <32874567+cabljac@users.noreply.github.com> Date: Tue, 24 Jun 2025 12:24:07 +0100 Subject: [PATCH 19/21] Update bin/install_cli Co-authored-by: Elliot Hesp --- bin/install_cli | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/install_cli b/bin/install_cli index 963ec0b16..35873ec60 100644 --- a/bin/install_cli +++ b/bin/install_cli @@ -230,7 +230,7 @@ case "$ARCH" in *) ARCH_SUFFIX="x64";; # Default to x64 esac -# Then we map the output to the names used on the Github releases page +# Then we map the output to the names used on the GitHub releases page case "$UNAME" in linux*) MACHINE="linux-${ARCH_SUFFIX}";; darwin*) MACHINE="darwin-${ARCH_SUFFIX}";; From 23ecf75297c2d1e07cbf11fee43c6804673dc727 Mon Sep 17 00:00:00 2001 From: Jacob Cable Date: Tue, 24 Jun 2025 13:21:58 +0100 Subject: [PATCH 20/21] refactor: clean up cross-runtime error handling and add tests --- genkit-tools/cli/package.json | 3 +- genkit-tools/cli/src/cli.ts | 9 +- .../cli/src/commands/ui-start-server.ts | 2 + genkit-tools/common/src/utils/errors.ts | 166 +++++++++-------- .../common/tests/utils/errors_test.ts | 172 ++++++++++++++++++ genkit-tools/pnpm-lock.yaml | 3 + 6 files changed, 276 insertions(+), 79 deletions(-) create mode 100644 genkit-tools/common/tests/utils/errors_test.ts diff --git a/genkit-tools/cli/package.json b/genkit-tools/cli/package.json index 247d1aaf8..485b0e607 100644 --- a/genkit-tools/cli/package.json +++ b/genkit-tools/cli/package.json @@ -17,7 +17,7 @@ "scripts": { "build": "pnpm genversion && tsc", "build:watch": "tsc --watch", - "compile:bun": "bun build src/bin/genkit.ts --compile --outfile dist/bin/genkit", + "compile:bun": "bun build src/bin/genkit.ts --compile --outfile dist/bin/genkit --minify", "test": "jest --verbose", "genversion": "genversion -esf src/utils/version.ts" }, @@ -47,6 +47,7 @@ "genversion": "^3.2.0", "jest": "^29.7.0", "ts-jest": "^29.1.2", + "ts-node": "^10.9.2", "typescript": "^5.3.3" } } diff --git a/genkit-tools/cli/src/cli.ts b/genkit-tools/cli/src/cli.ts index 54f638040..e44776ffd 100644 --- a/genkit-tools/cli/src/cli.ts +++ b/genkit-tools/cli/src/cli.ts @@ -31,7 +31,10 @@ import { flowRun } from './commands/flow-run'; import { getPluginCommands, getPluginSubCommand } from './commands/plugins'; import { start } from './commands/start'; import { uiStart } from './commands/ui-start'; -import { uiStartServer } from './commands/ui-start-server'; +import { + UI_START_SERVER_COMMAND, + uiStartServer, +} from './commands/ui-start-server'; import { uiStop } from './commands/ui-stop'; import { version } from './utils/version'; @@ -80,7 +83,9 @@ export async function startCLI(): Promise { await record(new RunCommandEvent(commandName)); }); - if (process.argv.includes('__ui:start-server')) { + // When running as a spawned UI server process, argv[1] will be '__ui:start-server' + // instead of a normal command. This allows the same binary to serve both CLI and server roles. + if (process.argv[1] === UI_START_SERVER_COMMAND) { program.addCommand(uiStartServer); } diff --git a/genkit-tools/cli/src/commands/ui-start-server.ts b/genkit-tools/cli/src/commands/ui-start-server.ts index ab4e86a62..65ca2a7a4 100644 --- a/genkit-tools/cli/src/commands/ui-start-server.ts +++ b/genkit-tools/cli/src/commands/ui-start-server.ts @@ -32,6 +32,8 @@ function redirectStdoutToFile(logFile: string) { process.stderr.write = process.stdout.write; } +export const UI_START_SERVER_COMMAND = '__ui:start-server' as const; + export const uiStartServer = new Command('__ui:start-server') .argument('', 'Port to serve on') .argument('', 'Log file path') diff --git a/genkit-tools/common/src/utils/errors.ts b/genkit-tools/common/src/utils/errors.ts index 32f2c39ec..de8abae39 100644 --- a/genkit-tools/common/src/utils/errors.ts +++ b/genkit-tools/common/src/utils/errors.ts @@ -1,5 +1,3 @@ -// genkit-tools/common/src/utils/errors.ts - /** * Copyright 2024 Google LLC * @@ -16,6 +14,26 @@ * limitations under the License. */ +// Connection error codes for different runtimes +const CONNECTION_ERROR_CODES = { + NODE_ECONNREFUSED: 'ECONNREFUSED', + BUN_CONNECTION_REFUSED: 'ConnectionRefused', + ECONNRESET: 'ECONNRESET', +} as const; + +const CONNECTION_ERROR_PATTERNS = [ + 'ECONNREFUSED', + 'Connection refused', + 'ConnectionRefused', + 'connect ECONNREFUSED', +] as const; + +type ErrorWithCode = { + code?: string; + message?: string; + cause?: ErrorWithCode; +}; + /** * Checks if an error is a connection refused error across Node.js and Bun runtimes. * @@ -27,58 +45,57 @@ export function isConnectionRefusedError(error: unknown): boolean { return false; } - // Handle plain objects with a code property (Bun fetch errors) - if (typeof error === 'object' && 'code' in error) { - const code = (error as any).code; - if ( - code === 'ECONNREFUSED' || // Node.js - code === 'ConnectionRefused' || // Bun - code === 'ECONNRESET' // Connection reset (also indicates server is down) - ) { - return true; - } + const errorCode = getErrorCode(error); + if (errorCode && isConnectionErrorCode(errorCode)) { + return true; } - // Handle Error instances - if (error instanceof Error) { - // Direct error code - if ('code' in error && typeof error.code === 'string') { - const code = error.code; - if ( - code === 'ECONNREFUSED' || - code === 'ConnectionRefused' || - code === 'ECONNRESET' - ) { - return true; - } - } - - // Node.js style with cause - if ( - 'cause' in error && - error.cause && - typeof error.cause === 'object' && - 'code' in error.cause && - error.cause.code === 'ECONNREFUSED' - ) { - return true; - } - - // Fallback: check error message - if ( - error.message && - (error.message.includes('ECONNREFUSED') || - error.message.includes('Connection refused') || - error.message.includes('ConnectionRefused') || - error.message.includes('connect ECONNREFUSED')) - ) { - return true; - } + // Fallback: check error message + if (isErrorWithMessage(error)) { + return CONNECTION_ERROR_PATTERNS.some((pattern) => + error.message.includes(pattern) + ); } return false; } +/** + * Helper function to check if a code is a connection error code. + */ +function isConnectionErrorCode(code: string): boolean { + return Object.values(CONNECTION_ERROR_CODES).includes( + code as (typeof CONNECTION_ERROR_CODES)[keyof typeof CONNECTION_ERROR_CODES] + ); +} + +/** + * Type guard to check if an error has a message property. + */ +function isErrorWithMessage(error: unknown): error is { message: string } { + return ( + typeof error === 'object' && + error !== null && + 'message' in error && + typeof (error as any).message === 'string' + ); +} + +/** + * Extracts error code from an object, handling nested structures. + */ +function extractErrorCode(obj: unknown): string | undefined { + if ( + typeof obj === 'object' && + obj !== null && + 'code' in obj && + typeof (obj as ErrorWithCode).code === 'string' + ) { + return (obj as ErrorWithCode).code; + } + return undefined; +} + /** * Gets the error code from an error object, handling both Node.js and Bun styles. */ @@ -87,32 +104,33 @@ export function getErrorCode(error: unknown): string | undefined { return undefined; } - // Handle plain objects with a code property - if ( - typeof error === 'object' && - 'code' in error && - typeof (error as any).code === 'string' - ) { - return (error as any).code; + // Direct error code + const directCode = extractErrorCode(error); + if (directCode) { + return directCode; } - // Handle Error instances - if (error instanceof Error) { - // Direct error code - if ('code' in error && typeof error.code === 'string') { - return error.code; + // Node.js style with cause + if (typeof error === 'object' && error !== null && 'cause' in error) { + const causeCode = extractErrorCode((error as ErrorWithCode).cause); + if (causeCode) { + return causeCode; } + } - // Node.js style with cause - if ( - 'cause' in error && - error.cause && - typeof error.cause === 'object' && - 'code' in error.cause && - typeof error.cause.code === 'string' - ) { - return error.cause.code; - } + return undefined; +} + +/** + * Extracts error message from various error formats. + */ +function extractErrorMessage(error: unknown): string | undefined { + if (error instanceof Error) { + return error.message; + } + + if (isErrorWithMessage(error)) { + return error.message; } return undefined; @@ -122,18 +140,14 @@ export function getErrorCode(error: unknown): string | undefined { * Safely extracts error details for logging. */ export function getErrorDetails(error: unknown): string { - if (!error) { + if (error === null || error === undefined) { return 'Unknown error'; } const code = getErrorCode(error); + const message = extractErrorMessage(error); - if (error instanceof Error) { - return code ? `${error.message} (${code})` : error.message; - } - - if (typeof error === 'object' && 'message' in error) { - const message = (error as any).message; + if (message) { return code ? `${message} (${code})` : message; } diff --git a/genkit-tools/common/tests/utils/errors_test.ts b/genkit-tools/common/tests/utils/errors_test.ts new file mode 100644 index 000000000..d1f14b010 --- /dev/null +++ b/genkit-tools/common/tests/utils/errors_test.ts @@ -0,0 +1,172 @@ +/** + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { describe, expect, it } from '@jest/globals'; +import { + getErrorCode, + getErrorDetails, + isConnectionRefusedError, +} from '../../src/utils/errors'; + +describe('errors.ts', () => { + describe('isConnectionRefusedError', () => { + it('should return false for null/undefined', () => { + expect(isConnectionRefusedError(null)).toBe(false); + expect(isConnectionRefusedError(undefined)).toBe(false); + }); + + it('should detect plain objects with connection error codes', () => { + expect(isConnectionRefusedError({ code: 'ECONNREFUSED' })).toBe(true); + expect(isConnectionRefusedError({ code: 'ConnectionRefused' })).toBe( + true + ); + expect(isConnectionRefusedError({ code: 'ECONNRESET' })).toBe(true); + expect(isConnectionRefusedError({ code: 'OTHER_ERROR' })).toBe(false); + }); + + it('should detect Error instances with direct code', () => { + const err = new Error('Connection failed'); + (err as any).code = 'ECONNREFUSED'; + expect(isConnectionRefusedError(err)).toBe(true); + + const err2 = new Error('Connection failed'); + (err2 as any).code = 'ConnectionRefused'; + expect(isConnectionRefusedError(err2)).toBe(true); + + const err3 = new Error('Connection failed'); + (err3 as any).code = 'ECONNRESET'; + expect(isConnectionRefusedError(err3)).toBe(true); + }); + + it('should detect Node.js style errors with cause', () => { + const err = new Error('Fetch failed'); + (err as any).cause = { code: 'ECONNREFUSED' }; + expect(isConnectionRefusedError(err)).toBe(true); + }); + + it('should fallback to checking error messages', () => { + expect( + isConnectionRefusedError( + new Error('connect ECONNREFUSED 127.0.0.1:3000') + ) + ).toBe(true); + expect( + isConnectionRefusedError(new Error('Connection refused to server')) + ).toBe(true); + expect( + isConnectionRefusedError( + new Error('ConnectionRefused: Unable to connect') + ) + ).toBe(true); + expect( + isConnectionRefusedError(new Error('Something else went wrong')) + ).toBe(false); + }); + + it('should handle complex nested structures', () => { + const err = new Error('Outer error'); + (err as any).cause = new Error('Inner error'); + (err as any).cause.code = 'ECONNREFUSED'; + expect(isConnectionRefusedError(err)).toBe(true); + }); + }); + + describe('getErrorCode', () => { + it('should return undefined for null/undefined', () => { + expect(getErrorCode(null)).toBeUndefined(); + expect(getErrorCode(undefined)).toBeUndefined(); + }); + + it('should extract code from plain objects', () => { + expect(getErrorCode({ code: 'ECONNREFUSED' })).toBe('ECONNREFUSED'); + expect(getErrorCode({ code: 'CUSTOM_ERROR' })).toBe('CUSTOM_ERROR'); + expect(getErrorCode({ message: 'No code here' })).toBeUndefined(); + }); + + it('should extract code from Error instances', () => { + const err = new Error('Test error'); + (err as any).code = 'TEST_CODE'; + expect(getErrorCode(err)).toBe('TEST_CODE'); + }); + + it('should extract code from cause property', () => { + const err = new Error('Outer error'); + (err as any).cause = { code: 'INNER_CODE' }; + expect(getErrorCode(err)).toBe('INNER_CODE'); + }); + + it('should prioritize direct code over cause code', () => { + const err = new Error('Test error'); + (err as any).code = 'DIRECT_CODE'; + (err as any).cause = { code: 'CAUSE_CODE' }; + expect(getErrorCode(err)).toBe('DIRECT_CODE'); + }); + + it('should handle non-string code values', () => { + expect(getErrorCode({ code: 123 })).toBeUndefined(); + expect(getErrorCode({ code: null })).toBeUndefined(); + expect(getErrorCode({ code: {} })).toBeUndefined(); + }); + }); + + describe('getErrorDetails', () => { + it('should return "Unknown error" for null/undefined', () => { + expect(getErrorDetails(null)).toBe('Unknown error'); + expect(getErrorDetails(undefined)).toBe('Unknown error'); + }); + + it('should format Error instances with code', () => { + const err = new Error('Connection failed'); + (err as any).code = 'ECONNREFUSED'; + expect(getErrorDetails(err)).toBe('Connection failed (ECONNREFUSED)'); + }); + + it('should format Error instances without code', () => { + const err = new Error('Simple error'); + expect(getErrorDetails(err)).toBe('Simple error'); + }); + + it('should format plain objects with message and code', () => { + expect(getErrorDetails({ message: 'Failed', code: 'ERR123' })).toBe( + 'Failed (ERR123)' + ); + expect(getErrorDetails({ message: 'No code here' })).toBe('No code here'); + }); + + it('should handle string errors', () => { + expect(getErrorDetails('String error')).toBe('String error'); + }); + + it('should handle number errors', () => { + expect(getErrorDetails(123)).toBe('123'); + }); + + it('should handle boolean errors', () => { + expect(getErrorDetails(true)).toBe('true'); + expect(getErrorDetails(false)).toBe('false'); + }); + + it('should handle objects without message', () => { + expect(getErrorDetails({ code: 'ERR_NO_MSG' })).toBe('[object Object]'); + }); + + it('should extract code from cause for formatting', () => { + const err = new Error('Outer error'); + (err as any).cause = { code: 'INNER_CODE' }; + expect(getErrorDetails(err)).toBe('Outer error (INNER_CODE)'); + }); + }); +}); diff --git a/genkit-tools/pnpm-lock.yaml b/genkit-tools/pnpm-lock.yaml index fa4b614de..ee4451938 100644 --- a/genkit-tools/pnpm-lock.yaml +++ b/genkit-tools/pnpm-lock.yaml @@ -87,6 +87,9 @@ importers: ts-jest: specifier: ^29.1.2 version: 29.4.0(@babel/core@7.24.5)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.5))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.1)(ts-node@10.9.2(@types/node@20.19.1)(typescript@5.8.3)))(typescript@5.8.3) + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@types/node@20.19.1)(typescript@5.8.3) typescript: specifier: ^5.3.3 version: 5.8.3 From cf2360bcb9b51a695ad892123455148bb2f74411 Mon Sep 17 00:00:00 2001 From: Jacob Cable <32874567+cabljac@users.noreply.github.com> Date: Tue, 24 Jun 2025 13:22:58 +0100 Subject: [PATCH 21/21] Update genkit-tools/common/src/utils/errors.ts --- genkit-tools/common/src/utils/errors.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/genkit-tools/common/src/utils/errors.ts b/genkit-tools/common/src/utils/errors.ts index de8abae39..00e3102d8 100644 --- a/genkit-tools/common/src/utils/errors.ts +++ b/genkit-tools/common/src/utils/errors.ts @@ -1,5 +1,5 @@ /** - * Copyright 2024 Google LLC + * Copyright 2025 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License.