From 4bd18c578eaf57e9a6aaff24fea5ea2ab1bbba16 Mon Sep 17 00:00:00 2001 From: Jacob Cable Date: Mon, 9 Jun 2025 18:02:04 +0100 Subject: [PATCH 01/25] 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 630c88ec8e..001230de95 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 9d36b70a5baff9e6c61ccd87099a9b3461a105be Mon Sep 17 00:00:00 2001 From: Jacob Cable Date: Mon, 9 Jun 2025 18:02:04 +0100 Subject: [PATCH 02/25] 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 001230de95..bc9a2e41ed 100644 --- a/genkit-tools/cli/package.json +++ b/genkit-tools/cli/package.json @@ -44,6 +44,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 2156f1c079..dbf2bdcd76 100644 --- a/genkit-tools/cli/src/cli.ts +++ b/genkit-tools/cli/src/cli.ts @@ -33,6 +33,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'; /** @@ -81,6 +82,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 071567c4b5..8fce423b7d 100644 --- a/genkit-tools/cli/src/commands/ui-start.ts +++ b/genkit-tools/cli/src/commands/ui-start.ts @@ -127,14 +127,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 67c73ff13e..081c93ff4c 100644 --- a/genkit-tools/cli/src/utils/server-harness.ts +++ b/genkit-tools/cli/src/utils/server-harness.ts @@ -16,22 +16,14 @@ import { startServer } from '@genkit-ai/tools-common/server'; import { findProjectRoot, logger } from '@genkit-ai/tools-common/utils'; +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(await findProjectRoot(), 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); @@ -41,14 +33,23 @@ function redirectStdoutToFile(logFile: string) { process.stderr.write = process.stdout.write; } -process.on('error', (error): void => { - logger.error(`Error in tools process: ${error}`); -}); -process.on('uncaughtException', (err, somethingelse) => { - logger.error(`Uncaught error in tools process: ${err} ${somethingelse}`); -}); -process.on('unhandledRejection', (reason, p) => { - logger.error(`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 => { + logger.error(`Error in tools process: ${error}`); + }); + process.on('uncaughtException', (err, somethingelse) => { + logger.error(`Uncaught error in tools process: ${err} ${somethingelse}`); + }); + process.on('unhandledRejection', (reason, p) => { + logger.error(`Unhandled rejection in tools process: ${reason}`); + }); + + const portNum = Number.parseInt(port) || 4100; + const manager = await startManager(await findProjectRoot(), true); + await startServer(manager, portNum); + }); diff --git a/genkit-tools/cli/tsconfig.json b/genkit-tools/cli/tsconfig.json index 4496203d6b..326dea5389 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 7ac45acba0..35354cb7e3 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 78105577b8..c50bd87e9c 100644 --- a/genkit-tools/pnpm-lock.yaml +++ b/genkit-tools/pnpm-lock.yaml @@ -78,6 +78,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 @@ -205,6 +208,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 @@ -1265,6 +1271,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'} @@ -4539,6 +4548,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 6fd3d222a4771305d7b4b8ac9b75c217a147ba2f Mon Sep 17 00:00:00 2001 From: Jacob Cable Date: Wed, 18 Jun 2025 12:27:38 +0100 Subject: [PATCH 03/25] 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 0000000000..32f2c39ecd --- /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 5cf26d9c35..aeab3fb57e 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 { @@ -143,10 +144,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; } } @@ -187,10 +185,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 90ae4da62d..df69be93dc 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 26825f84f1e5fa8b1b0508f13d02efc760d68500 Mon Sep 17 00:00:00 2001 From: Jacob Cable Date: Wed, 18 Jun 2025 14:24:45 +0100 Subject: [PATCH 04/25] 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 bc9a2e41ed..8fd543fbb4 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 326dea5389..3673086655 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 35354cb7e3..ef6e01063a 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 7e94aef735..88561673dc 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 b63e7bcd20..035ea1b991 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 3d3febb41bc9a6e0dfa49afad830189c30ecbbd9 Mon Sep 17 00:00:00 2001 From: Jacob Cable Date: Wed, 18 Jun 2025 14:43:19 +0100 Subject: [PATCH 05/25] 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 0000000000..2613133c98 --- /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 88929426e31429b5545e80767f4eccfec0c61a85 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/25] 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 df69be93dc..fa2882d8e8 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 a744798116ea1ade2459534deb3a66ad694e37ec Mon Sep 17 00:00:00 2001 From: Jacob Cable Date: Wed, 18 Jun 2025 14:52:08 +0100 Subject: [PATCH 07/25] 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 fa2882d8e8..90ae4da62d 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 408fadbad3818ad8656c88621cbd814bd7fd92d8 Mon Sep 17 00:00:00 2001 From: Jacob Cable Date: Wed, 18 Jun 2025 14:59:32 +0100 Subject: [PATCH 08/25] 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 2613133c98..4264b3285c 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 eacf3a7d4e948f183bbe4c5d3764f7561f121fcd Mon Sep 17 00:00:00 2001 From: Jacob Cable Date: Wed, 18 Jun 2025 16:30:07 +0100 Subject: [PATCH 09/25] 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 4264b3285c..f486747072 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 0000000000..af2c6bdc6c --- /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 b1cf237e00c3808d8fcf3561c32c582350362733 Mon Sep 17 00:00:00 2001 From: Jacob Cable Date: Fri, 20 Jun 2025 10:04:43 +0100 Subject: [PATCH 10/25] 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 f486747072..2ddb285ca0 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 f58ce1f4487ac93abd310069b00fe4c234fd3af5 Mon Sep 17 00:00:00 2001 From: Jacob Cable Date: Fri, 20 Jun 2025 10:07:13 +0100 Subject: [PATCH 11/25] 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 2ddb285ca0..c7246213d2 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 7457eccb3821bf9c5f39ffd23903e79ed2c6289e Mon Sep 17 00:00:00 2001 From: Jacob Cable Date: Fri, 20 Jun 2025 11:46:50 +0100 Subject: [PATCH 12/25] 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 c7246213d2..1c477c6b5f 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 1fcf4c800a0e6a775deadc43cb80b44ca1e590b3 Mon Sep 17 00:00:00 2001 From: Jacob Cable Date: Fri, 20 Jun 2025 12:23:03 +0100 Subject: [PATCH 13/25] 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 1c477c6b5f..95776d3cba 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 2c2cbd311229fff0ae2652783f50afaf23791f62 Mon Sep 17 00:00:00 2001 From: Jacob Cable Date: Fri, 20 Jun 2025 12:44:41 +0100 Subject: [PATCH 14/25] 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 95776d3cba..3b16809358 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 5512e46bfd5cb136a89f9e51a0a55efec9211d91 Mon Sep 17 00:00:00 2001 From: Jacob Cable Date: Mon, 23 Jun 2025 10:44:19 +0100 Subject: [PATCH 15/25] 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 dbf2bdcd76..4f3795073b 100644 --- a/genkit-tools/cli/src/cli.ts +++ b/genkit-tools/cli/src/cli.ts @@ -32,8 +32,8 @@ import { mcp } from './commands/mcp'; 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 081c93ff4c..c8633fc49c 100644 --- a/genkit-tools/cli/src/utils/server-harness.ts +++ b/genkit-tools/cli/src/commands/ui-start-server.ts @@ -18,7 +18,7 @@ import { startServer } from '@genkit-ai/tools-common/server'; import { findProjectRoot, logger } from '@genkit-ai/tools-common/utils'; 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 72f8beb9c58973cd93fccba93861b0dda2e7b047 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/25] 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 af2c6bdc6c..4e24227972 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 7597d308781d8aa1e0a5fdc34b3000b77b35ae15 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/25] 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 4e24227972..51bd72a78f 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 72a360b575fc348ddcd0284515e587a7e2d93f38 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/25] 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 51bd72a78f..963ec0b167 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 de01c56fb9a48ce1ef0a4cf1f57af1e68b659d40 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/25] 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 963ec0b167..35873ec603 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 25f8c12eb508a1360ae587fa7829a7a91a16fd38 Mon Sep 17 00:00:00 2001 From: Jacob Cable Date: Tue, 24 Jun 2025 13:21:58 +0100 Subject: [PATCH 20/25] 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 8fd543fbb4..23c822940e 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" }, @@ -48,6 +48,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 4f3795073b..45f094a80a 100644 --- a/genkit-tools/cli/src/cli.ts +++ b/genkit-tools/cli/src/cli.ts @@ -32,7 +32,10 @@ import { mcp } from './commands/mcp'; 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'; @@ -82,7 +85,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 c8633fc49c..aedf24dfb4 100644 --- a/genkit-tools/cli/src/commands/ui-start-server.ts +++ b/genkit-tools/cli/src/commands/ui-start-server.ts @@ -33,6 +33,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 32f2c39ecd..de8abae395 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 0000000000..d1f14b0100 --- /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 c50bd87e9c..efa410440b 100644 --- a/genkit-tools/pnpm-lock.yaml +++ b/genkit-tools/pnpm-lock.yaml @@ -90,6 +90,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 b58fd839c6655b60887d14b15b9e70e091f20600 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/25] 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 de8abae395..00e3102d8d 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. From b9d14e6632e77468007d0d126cd40142955629c0 Mon Sep 17 00:00:00 2001 From: Jacob Cable Date: Fri, 4 Jul 2025 07:50:49 +0100 Subject: [PATCH 22/25] refactor(cli): change name of internal command --- genkit-tools/cli/src/cli.ts | 14 +++++++------- .../{ui-start-server.ts => server-harness.ts} | 4 ++-- genkit-tools/cli/src/commands/ui-start.ts | 3 ++- 3 files changed, 11 insertions(+), 10 deletions(-) rename genkit-tools/cli/src/commands/{ui-start-server.ts => server-harness.ts} (93%) diff --git a/genkit-tools/cli/src/cli.ts b/genkit-tools/cli/src/cli.ts index 45f094a80a..4b151ba2aa 100644 --- a/genkit-tools/cli/src/cli.ts +++ b/genkit-tools/cli/src/cli.ts @@ -30,12 +30,12 @@ import { flowBatchRun } from './commands/flow-batch-run'; import { flowRun } from './commands/flow-run'; import { mcp } from './commands/mcp'; import { getPluginCommands, getPluginSubCommand } from './commands/plugins'; +import { + SERVER_HARNESS_COMMAND, + serverHarness, +} from './commands/server-harness'; import { start } from './commands/start'; import { uiStart } from './commands/ui-start'; -import { - UI_START_SERVER_COMMAND, - uiStartServer, -} from './commands/ui-start-server'; import { uiStop } from './commands/ui-stop'; import { version } from './utils/version'; @@ -85,10 +85,10 @@ export async function startCLI(): Promise { await record(new RunCommandEvent(commandName)); }); - // When running as a spawned UI server process, argv[1] will be '__ui:start-server' + // When running as a spawned UI server process, argv[1] will be '__server-harness' // 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); + if (process.argv[1] === SERVER_HARNESS_COMMAND) { + program.addCommand(serverHarness); } for (const command of commands) program.addCommand(command); diff --git a/genkit-tools/cli/src/commands/ui-start-server.ts b/genkit-tools/cli/src/commands/server-harness.ts similarity index 93% rename from genkit-tools/cli/src/commands/ui-start-server.ts rename to genkit-tools/cli/src/commands/server-harness.ts index aedf24dfb4..5ade55f7a9 100644 --- a/genkit-tools/cli/src/commands/ui-start-server.ts +++ b/genkit-tools/cli/src/commands/server-harness.ts @@ -33,9 +33,9 @@ function redirectStdoutToFile(logFile: string) { process.stderr.write = process.stdout.write; } -export const UI_START_SERVER_COMMAND = '__ui:start-server' as const; +export const SERVER_HARNESS_COMMAND = '__server-harness' as const; -export const uiStartServer = new Command('__ui:start-server') +export const serverHarness = new Command('__server-harness') .argument('', 'Port to serve on') .argument('', 'Log file path') .action(async (port: string, logFile: string) => { diff --git a/genkit-tools/cli/src/commands/ui-start.ts b/genkit-tools/cli/src/commands/ui-start.ts index 8fce423b7d..08d48f745f 100644 --- a/genkit-tools/cli/src/commands/ui-start.ts +++ b/genkit-tools/cli/src/commands/ui-start.ts @@ -30,6 +30,7 @@ import fs from 'fs/promises'; import getPort, { makeRange } from 'get-port'; import open from 'open'; import path from 'path'; +import { SERVER_HARNESS_COMMAND } from './server-harness'; interface StartOptions { port: string; @@ -129,7 +130,7 @@ async function startAndWaitUntilHealthy( return new Promise((resolve, reject) => { const child = spawn( process.execPath, - ['__ui:start-server', port.toString(), serversDir + '/devui.log'], + [SERVER_HARNESS_COMMAND, port.toString(), serversDir + '/devui.log'], { stdio: ['ignore', 'ignore', 'ignore'], } From 421820f52c382a9466b023ddecd43d8c0d49cde6 Mon Sep 17 00:00:00 2001 From: Jacob Cable Date: Fri, 4 Jul 2025 08:46:35 +0100 Subject: [PATCH 23/25] fix(cli): adjust check for internal command --- genkit-tools/cli/src/cli.ts | 2 +- genkit-tools/cli/src/commands/server-harness.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/genkit-tools/cli/src/cli.ts b/genkit-tools/cli/src/cli.ts index 4b151ba2aa..5220ac044e 100644 --- a/genkit-tools/cli/src/cli.ts +++ b/genkit-tools/cli/src/cli.ts @@ -87,7 +87,7 @@ export async function startCLI(): Promise { // When running as a spawned UI server process, argv[1] will be '__server-harness' // instead of a normal command. This allows the same binary to serve both CLI and server roles. - if (process.argv[1] === SERVER_HARNESS_COMMAND) { + if (process.argv[2] === SERVER_HARNESS_COMMAND) { program.addCommand(serverHarness); } diff --git a/genkit-tools/cli/src/commands/server-harness.ts b/genkit-tools/cli/src/commands/server-harness.ts index 5ade55f7a9..f03a4ce570 100644 --- a/genkit-tools/cli/src/commands/server-harness.ts +++ b/genkit-tools/cli/src/commands/server-harness.ts @@ -47,7 +47,7 @@ export const serverHarness = new Command('__server-harness') process.on('uncaughtException', (err, somethingelse) => { logger.error(`Uncaught error in tools process: ${err} ${somethingelse}`); }); - process.on('unhandledRejection', (reason, p) => { + process.on('unhandledRejection', (reason, _p) => { logger.error(`Unhandled rejection in tools process: ${reason}`); }); From 950a11588b8d318267667b09add4091cd9705b64 Mon Sep 17 00:00:00 2001 From: Jacob Cable Date: Thu, 10 Jul 2025 10:10:50 +0100 Subject: [PATCH 24/25] fix: make workflow a manual trigger --- .github/workflows/build-cli-binaries.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/build-cli-binaries.yml b/.github/workflows/build-cli-binaries.yml index 3b16809358..828c6fd718 100644 --- a/.github/workflows/build-cli-binaries.yml +++ b/.github/workflows/build-cli-binaries.yml @@ -17,9 +17,6 @@ name: Build CLI Binaries on: - push: - branches: - - '@invertase/cli-binary' workflow_dispatch: jobs: From 98d52530973c236c93964c4e1579cd4d19798c20 Mon Sep 17 00:00:00 2001 From: Jacob Cable Date: Thu, 10 Jul 2025 10:13:29 +0100 Subject: [PATCH 25/25] feat: automatically update latest tag for cli releases --- .github/workflows/build-cli-binaries.yml | 152 ++++++++++++++++++++++- bin/install_cli | 3 +- 2 files changed, 153 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-cli-binaries.yml b/.github/workflows/build-cli-binaries.yml index 828c6fd718..0366162cce 100644 --- a/.github/workflows/build-cli-binaries.yml +++ b/.github/workflows/build-cli-binaries.yml @@ -18,6 +18,16 @@ name: Build CLI Binaries on: workflow_dispatch: + inputs: + version: + description: 'Version tag to build (e.g., v1.0.0)' + required: true + type: string + upload_to_release: + description: 'Upload binaries to GitHub release and update latest tag' + required: false + type: boolean + default: false jobs: build: @@ -46,6 +56,15 @@ jobs: - name: Checkout code uses: actions/checkout@v4 + - name: Validate version format + run: | + VERSION="${{ inputs.version }}" + if [[ ! "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$ ]]; then + echo "Error: Version '$VERSION' does not follow semantic versioning format (e.g., v1.0.0, v1.0.0-beta.1)" + exit 1 + fi + echo "✓ Version format is valid: $VERSION" + - name: Setup Bun uses: oven-sh/setup-bun@v2 with: @@ -253,4 +272,135 @@ jobs: } # Clean up any remaining genkit processes - Get-Process | Where-Object { $_.ProcessName -match "genkit" } | Stop-Process -Force -ErrorAction SilentlyContinue \ No newline at end of file + Get-Process | Where-Object { $_.ProcessName -match "genkit" } | Stop-Process -Force -ErrorAction SilentlyContinue + + create-release: + needs: [build, test] + runs-on: ubuntu-latest + if: inputs.upload_to_release == 'true' + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Generate changelog + id: changelog + run: | + # Get the previous release tag + PREVIOUS_TAG=$(git describe --tags --abbrev=0 HEAD~1 2>/dev/null || echo "") + + if [[ -n "$PREVIOUS_TAG" ]]; then + # Generate changelog from previous tag to current + CHANGELOG=$(git log --pretty=format:"- %s" $PREVIOUS_TAG..HEAD | head -20) + echo "changelog<> $GITHUB_OUTPUT + echo "$CHANGELOG" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + else + # First release + echo "changelog<> $GITHUB_OUTPUT + echo "- Initial release" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + fi + + - name: Create Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ inputs.version }} + release_name: Genkit CLI ${{ inputs.version }} + body: | + # Genkit CLI ${{ inputs.version }} + + ## Downloads + + - [Linux x64](https://github.com/firebase/genkit/releases/download/${{ inputs.version }}/genkit-linux-x64) + - [Linux ARM64](https://github.com/firebase/genkit/releases/download/${{ inputs.version }}/genkit-linux-arm64) + - [macOS x64](https://github.com/firebase/genkit/releases/download/${{ inputs.version }}/genkit-darwin-x64) + - [macOS ARM64](https://github.com/firebase/genkit/releases/download/${{ inputs.version }}/genkit-darwin-arm64) + - [Windows x64](https://github.com/firebase/genkit/releases/download/${{ inputs.version }}/genkit-win32-x64.exe) + + ## Changes + + ${{ steps.changelog.outputs.changelog }} + + ## Installation + + ```bash + TODO: Add installation instructions + ``` + draft: false + prerelease: false + + upload-assets: + needs: [build, test, create-release] + runs-on: ubuntu-latest + if: inputs.upload_to_release == 'true' + strategy: + matrix: + include: + - target: linux-x64 + - target: linux-arm64 + - target: darwin-x64 + - target: darwin-arm64 + - target: win32-x64 + + 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: Upload to GitHub Release + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ needs.create-release.outputs.upload_url }} + asset_path: ./genkit-${{ matrix.target }}${{ steps.binary.outputs.ext }} + asset_name: genkit-${{ matrix.target }} + asset_content_type: application/octet-stream + + update-latest-tag: + needs: [create-release, upload-assets] + runs-on: ubuntu-latest + if: inputs.upload_to_release == 'true' + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Update latest tag + run: | + # Check if latest tag already points to the correct version + CURRENT_LATEST=$(git rev-parse latest 2>/dev/null || echo "") + TARGET_COMMIT=$(git rev-parse ${{ inputs.version }} 2>/dev/null || echo "") + + if [[ "$CURRENT_LATEST" == "$TARGET_COMMIT" ]]; then + echo "Latest tag already points to ${{ inputs.version }}" + exit 0 + fi + + # Delete the existing "latest" tag if it exists + git tag -d latest 2>/dev/null || true + git push origin :refs/tags/latest 2>/dev/null || true + + # Create new "latest" tag pointing to the release tag + git tag latest ${{ inputs.version }} + git push origin latest + + echo "Updated 'latest' tag to point to ${{ inputs.version }}" \ No newline at end of file diff --git a/bin/install_cli b/bin/install_cli index 35873ec603..1d5fc263dc 100644 --- a/bin/install_cli +++ b/bin/install_cli @@ -250,7 +250,8 @@ if [[ -z "$MACHINE" ]]; then fi # We have enough information to generate the binary's download URL. -DOWNLOAD_URL="https://$DOMAIN/bin/$MACHINE/latest" +# Use GitHub releases with "latest" tag that gets updated by the workflow +DOWNLOAD_URL="https://github.com/firebase/genkit/releases/download/latest/genkit-$MACHINE" echo "-- Downloading binary from $DOWNLOAD_URL" # We use "curl" to download the binary with a flag set to follow redirects