From a3a23eb9e9b5ede851d431a961d57783fdc2d1c6 Mon Sep 17 00:00:00 2001 From: Samuel Bushi Date: Thu, 8 Jan 2026 13:13:56 -0500 Subject: [PATCH 1/6] enable mcp logs, noninteractive --- genkit-tools/cli/src/commands/mcp.ts | 8 ++++++-- genkit-tools/cli/src/mcp/runtime.ts | 8 ++++++-- genkit-tools/cli/src/mcp/util.ts | 3 ++- genkit-tools/cli/src/utils/manager-utils.ts | 3 ++- genkit-tools/common/src/manager/manager.ts | 1 + genkit-tools/common/src/manager/process-manager.ts | 12 ++++++++++-- genkit-tools/common/src/utils/logger.ts | 13 +++++++++++++ 7 files changed, 40 insertions(+), 8 deletions(-) diff --git a/genkit-tools/cli/src/commands/mcp.ts b/genkit-tools/cli/src/commands/mcp.ts index 02a007c128..6d8fe26014 100644 --- a/genkit-tools/cli/src/commands/mcp.ts +++ b/genkit-tools/cli/src/commands/mcp.ts @@ -14,19 +14,23 @@ * limitations under the License. */ -import { findProjectRoot, forceStderr } from '@genkit-ai/tools-common/utils'; +import { debugToFile, findProjectRoot } from '@genkit-ai/tools-common/utils'; import { Command } from 'commander'; import { startMcpServer } from '../mcp/server'; interface McpOptions { projectRoot?: string; + debug?: boolean; } /** Command to run MCP server. */ export const mcp = new Command('mcp') .option('--project-root [projectRoot]', 'Project root') + .option('-d, --debug', 'debug to file', false) .description('run MCP stdio server (EXPERIMENTAL, subject to change)') .action(async (options: McpOptions) => { - forceStderr(); + if (options.debug) { + debugToFile(); + } await startMcpServer(options.projectRoot ?? (await findProjectRoot())); }); diff --git a/genkit-tools/cli/src/mcp/runtime.ts b/genkit-tools/cli/src/mcp/runtime.ts index daa52e622d..aeb53709e6 100644 --- a/genkit-tools/cli/src/mcp/runtime.ts +++ b/genkit-tools/cli/src/mcp/runtime.ts @@ -34,8 +34,12 @@ export function defineRuntimeTools( {command: 'go', args: ['run', 'main.go']} {command: 'npm', args: ['run', 'dev']}`, inputSchema: { - command: z.string(), - args: z.array(z.string()), + command: z.string().describe('The command to run'), + args: z + .array(z.string()) + .describe( + 'List of command line arguments. IMPORTANT: This must be a JSON array of strings, not a single string.' + ), }, }, async ({ command, args }) => { diff --git a/genkit-tools/cli/src/mcp/util.ts b/genkit-tools/cli/src/mcp/util.ts index f39ac569bf..4089023478 100644 --- a/genkit-tools/cli/src/mcp/util.ts +++ b/genkit-tools/cli/src/mcp/util.ts @@ -41,7 +41,8 @@ export class McpRuntimeManager { const devManager = await startDevProcessManager( this.projectRoot, command, - args + args, + { nonInteractive: true } ); this.manager = devManager.manager; return this.manager; diff --git a/genkit-tools/cli/src/utils/manager-utils.ts b/genkit-tools/cli/src/utils/manager-utils.ts index 187e32a116..1f5d63b92a 100644 --- a/genkit-tools/cli/src/utils/manager-utils.ts +++ b/genkit-tools/cli/src/utils/manager-utils.ts @@ -68,6 +68,7 @@ export async function startManager( export interface DevProcessManagerOptions { disableRealtimeTelemetry?: boolean; + nonInteractive?: boolean; } export async function startDevProcessManager( @@ -93,7 +94,7 @@ export async function startDevProcessManager( processManager, disableRealtimeTelemetry, }); - const processPromise = processManager.start(); + const processPromise = processManager.start(options); return { manager, processPromise }; } diff --git a/genkit-tools/common/src/manager/manager.ts b/genkit-tools/common/src/manager/manager.ts index 7b20698644..8042ca96ee 100644 --- a/genkit-tools/common/src/manager/manager.ts +++ b/genkit-tools/common/src/manager/manager.ts @@ -567,6 +567,7 @@ export class RuntimeManager { try { const runtimesDir = await findRuntimesDir(this.projectRoot); await fs.mkdir(runtimesDir, { recursive: true }); + logger.debug(`Watching runtimes in ${runtimesDir}`); const watcher = chokidar.watch(runtimesDir, { persistent: true, ignoreInitial: false, diff --git a/genkit-tools/common/src/manager/process-manager.ts b/genkit-tools/common/src/manager/process-manager.ts index 3c4f905fb7..79cd3f2d10 100644 --- a/genkit-tools/common/src/manager/process-manager.ts +++ b/genkit-tools/common/src/manager/process-manager.ts @@ -47,6 +47,7 @@ export class ProcessManager { * Starts the process. */ start(options?: ProcessManagerStartOptions): Promise { + logger.debug(`Starting process: ${this.command} ${this.args.join(' ')}`); return new Promise((resolve, reject) => { this._status = 'running'; this.appProcess = spawn(this.command, this.args, { @@ -62,6 +63,13 @@ export class ProcessManager { this.appProcess.stderr?.pipe(process.stderr); this.appProcess.stdout?.pipe(process.stdout); process.stdin?.pipe(this.appProcess.stdin!); + } else { + this.appProcess.stderr?.on('data', (data) => { + logger.debug(`[ProcessManager Stderr] ${data.toString()}`); + }); + this.appProcess.stdout?.on('data', (data) => { + logger.debug(`[ProcessManager Stdout] ${data.toString()}`); + }); } this.appProcess.on('error', (error): void => { @@ -133,8 +141,8 @@ export class ProcessManager { this.originalStdIn = undefined; } if (this.appProcess) { - this.appProcess.stdout?.unpipe(process.stdout); - this.appProcess.stderr?.unpipe(process.stderr); + this.appProcess.stdout?.unpipe(); + this.appProcess.stderr?.unpipe(); } this.appProcess = undefined; this._status = 'stopped'; diff --git a/genkit-tools/common/src/utils/logger.ts b/genkit-tools/common/src/utils/logger.ts index 0e2a62e502..0d6a211223 100644 --- a/genkit-tools/common/src/utils/logger.ts +++ b/genkit-tools/common/src/utils/logger.ts @@ -29,6 +29,19 @@ export function forceStderr() { ); } +export function debugToFile() { + logger.add( + new winston.transports.File({ + filename: 'genkit-debug.log', + format: winston.format.combine( + winston.format.timestamp(), + winston.format.json() + ), + }) + ); + logger.level = 'debug'; +} + export const logger = winston.createLogger({ level: process.env.DEBUG ? 'debug' : 'info', format: winston.format.printf((log) => { From 1765a7cb1ee1795099123b293f138d7c57f62f31 Mon Sep 17 00:00:00 2001 From: Samuel Bushi Date: Fri, 9 Jan 2026 16:40:23 -0500 Subject: [PATCH 2/6] fixes, tests, logs --- genkit-tools/cli/src/commands/mcp.ts | 7 ++- genkit-tools/cli/src/mcp/server.ts | 1 + genkit-tools/cli/src/utils/manager-utils.ts | 55 ++++++++++++++++++- genkit-tools/common/src/manager/manager.ts | 16 +++++- .../common/src/manager/process-manager.ts | 2 + js/core/src/reflection.ts | 7 ++- 6 files changed, 81 insertions(+), 7 deletions(-) diff --git a/genkit-tools/cli/src/commands/mcp.ts b/genkit-tools/cli/src/commands/mcp.ts index 6d8fe26014..3b6e4eb1ca 100644 --- a/genkit-tools/cli/src/commands/mcp.ts +++ b/genkit-tools/cli/src/commands/mcp.ts @@ -14,7 +14,11 @@ * limitations under the License. */ -import { debugToFile, findProjectRoot } from '@genkit-ai/tools-common/utils'; +import { + debugToFile, + findProjectRoot, + forceStderr, +} from '@genkit-ai/tools-common/utils'; import { Command } from 'commander'; import { startMcpServer } from '../mcp/server'; @@ -29,6 +33,7 @@ export const mcp = new Command('mcp') .option('-d, --debug', 'debug to file', false) .description('run MCP stdio server (EXPERIMENTAL, subject to change)') .action(async (options: McpOptions) => { + forceStderr(); if (options.debug) { debugToFile(); } diff --git a/genkit-tools/cli/src/mcp/server.ts b/genkit-tools/cli/src/mcp/server.ts index 6aecf2a218..9bbd03a44f 100644 --- a/genkit-tools/cli/src/mcp/server.ts +++ b/genkit-tools/cli/src/mcp/server.ts @@ -26,6 +26,7 @@ import { defineUsageGuideTool } from './usage'; import { McpRuntimeManager } from './util'; export async function startMcpServer(projectRoot: string) { + logger.info(`Starting MCP server in: ${projectRoot}`); const server = new McpServer({ name: 'Genkit MCP', version: '0.0.2', diff --git a/genkit-tools/cli/src/utils/manager-utils.ts b/genkit-tools/cli/src/utils/manager-utils.ts index 1f5d63b92a..a7c44062d1 100644 --- a/genkit-tools/cli/src/utils/manager-utils.ts +++ b/genkit-tools/cli/src/utils/manager-utils.ts @@ -21,10 +21,12 @@ import { import type { Status } from '@genkit-ai/tools-common'; import { ProcessManager, + RuntimeEvent, RuntimeManager, type GenkitToolsError, } from '@genkit-ai/tools-common/manager'; import { logger } from '@genkit-ai/tools-common/utils'; +import * as crypto from 'crypto'; import getPort, { makeRange } from 'get-port'; /** @@ -79,9 +81,11 @@ export async function startDevProcessManager( ): Promise<{ manager: RuntimeManager; processPromise: Promise }> { const telemetryServerUrl = await resolveTelemetryServer(projectRoot); const disableRealtimeTelemetry = options?.disableRealtimeTelemetry ?? false; + const runtimeId = crypto.randomUUID().substring(0, 8); const envVars: Record = { GENKIT_TELEMETRY_SERVER: telemetryServerUrl, GENKIT_ENV: 'dev', + GENKIT_RUNTIME_ID: runtimeId, }; if (!disableRealtimeTelemetry) { envVars.GENKIT_ENABLE_REALTIME_TELEMETRY = 'true'; @@ -94,10 +98,59 @@ export async function startDevProcessManager( processManager, disableRealtimeTelemetry, }); - const processPromise = processManager.start(options); + const processPromise = processManager.start({ ...options, cwd: projectRoot }); + + await waitForRuntime(manager, runtimeId, processPromise); + return { manager, processPromise }; } +/** + * Waits for the runtime with the given ID to register itself. + * Rejects if the process exits or if the timeout is reached. + */ +export async function waitForRuntime( + manager: RuntimeManager, + runtimeId: string, + processPromise: Promise +): Promise { + if (manager.getRuntimeById(runtimeId)) { + return; + } + + await new Promise((resolve, reject) => { + let timeoutId: NodeJS.Timeout; + let unsubscribe: () => void; + + const cleanup = () => { + if (timeoutId) clearTimeout(timeoutId); + if (unsubscribe) unsubscribe(); + }; + + timeoutId = setTimeout(() => { + cleanup(); + reject(new Error('Timeout waiting for runtime to be ready')); + }, 30000); + + unsubscribe = manager.onRuntimeEvent((event, runtime) => { + if (event === RuntimeEvent.ADD && runtime.id === runtimeId) { + cleanup(); + resolve(); + } + }); + + processPromise + .then(() => { + cleanup(); + reject(new Error('Process exited before runtime was ready')); + }) + .catch((err) => { + cleanup(); + reject(err); + }); + }); +} + /** * Runs the given function with a runtime manager. */ diff --git a/genkit-tools/common/src/manager/manager.ts b/genkit-tools/common/src/manager/manager.ts index 8042ca96ee..1f28d303ad 100644 --- a/genkit-tools/common/src/manager/manager.ts +++ b/genkit-tools/common/src/manager/manager.ts @@ -167,13 +167,23 @@ export class RuntimeManager { * `runtime` to which it applies. * * @param listener the callback function. + * @returns an unsubscriber function. */ onRuntimeEvent( listener: (eventType: RuntimeEvent, runtime: RuntimeInfo) => void ) { - Object.values(RuntimeEvent).forEach((event) => - this.eventEmitter.on(event, (rt) => listener(event, rt)) - ); + const listeners: Array<{ event: string; fn: (rt: RuntimeInfo) => void }> = + []; + Object.values(RuntimeEvent).forEach((event) => { + const fn = (rt: RuntimeInfo) => listener(event, rt); + this.eventEmitter.on(event, fn); + listeners.push({ event, fn }); + }); + return () => { + listeners.forEach(({ event, fn }) => { + this.eventEmitter.off(event, fn); + }); + }; } /** diff --git a/genkit-tools/common/src/manager/process-manager.ts b/genkit-tools/common/src/manager/process-manager.ts index 79cd3f2d10..308fa10137 100644 --- a/genkit-tools/common/src/manager/process-manager.ts +++ b/genkit-tools/common/src/manager/process-manager.ts @@ -25,6 +25,7 @@ export interface AppProcessStatus { } export interface ProcessManagerStartOptions { + cwd?: string; nonInteractive?: boolean; } @@ -51,6 +52,7 @@ export class ProcessManager { return new Promise((resolve, reject) => { this._status = 'running'; this.appProcess = spawn(this.command, this.args, { + cwd: options?.cwd, env: { ...process.env, ...this.env, diff --git a/js/core/src/reflection.ts b/js/core/src/reflection.ts index 9b9389ad9d..5d6bf6f667 100644 --- a/js/core/src/reflection.ts +++ b/js/core/src/reflection.ts @@ -103,7 +103,10 @@ export class ReflectionServer { } get runtimeId() { - return `${process.pid}${this.port !== null ? `-${this.port}` : ''}`; + return ( + process.env.GENKIT_RUNTIME_ID ?? + `${process.pid}${this.port !== null ? `-${this.port}` : ''}` + ); } /** @@ -438,7 +441,7 @@ export class ReflectionServer { ); const fileContent = JSON.stringify( { - id: process.env.GENKIT_RUNTIME_ID || this.runtimeId, + id: this.runtimeId, pid: process.pid, name: this.options.name, reflectionServerUrl: `http://localhost:${this.port}`, From c26e5ff44572ebeeacbf602b1b4217512068875a Mon Sep 17 00:00:00 2001 From: Samuel Bushi Date: Fri, 9 Jan 2026 16:40:30 -0500 Subject: [PATCH 3/6] fixes, tests, logs --- genkit-tools/cli/tests/commands/start_test.ts | 117 ++++++++++++++++++ .../cli/tests/utils/manager-utils_test.ts | 100 +++++++++++++++ genkit-tools/common/tests/manager_test.ts | 47 +++++++ 3 files changed, 264 insertions(+) create mode 100644 genkit-tools/cli/tests/commands/start_test.ts create mode 100644 genkit-tools/cli/tests/utils/manager-utils_test.ts create mode 100644 genkit-tools/common/tests/manager_test.ts diff --git a/genkit-tools/cli/tests/commands/start_test.ts b/genkit-tools/cli/tests/commands/start_test.ts new file mode 100644 index 0000000000..e93509d0c6 --- /dev/null +++ b/genkit-tools/cli/tests/commands/start_test.ts @@ -0,0 +1,117 @@ +/** + * 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. + */ + +import { startServer } from '@genkit-ai/tools-common/server'; +import { + afterEach, + beforeEach, + describe, + expect, + it, + jest, +} from '@jest/globals'; +import { start } from '../../src/commands/start'; +import * as managerUtils from '../../src/utils/manager-utils'; + +jest.mock('@genkit-ai/tools-common/server'); +jest.mock('@genkit-ai/tools-common/utils', () => ({ + findProjectRoot: jest.fn(() => Promise.resolve('/mock/root')), + logger: { + warn: jest.fn(), + error: jest.fn(), + }, +})); +jest.mock('get-port', () => ({ + __esModule: true, + default: jest.fn(() => Promise.resolve(4000)), + makeRange: jest.fn(), +})); +jest.mock('open'); + +describe('start command', () => { + let startDevProcessManagerSpy: any; + let startManagerSpy: any; + let startServerSpy: any; + + beforeEach(() => { + startDevProcessManagerSpy = jest + .spyOn(managerUtils, 'startDevProcessManager') + .mockResolvedValue({ + manager: {} as any, + processPromise: Promise.resolve(), + }); + startManagerSpy = jest + .spyOn(managerUtils, 'startManager') + .mockResolvedValue({} as any); + startServerSpy = startServer as unknown as jest.Mock; + + // Reset args + start.args = []; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should start dev process manager when args are provided', async () => { + await start.parseAsync(['node', 'genkit', 'run', 'app']); + + expect(startDevProcessManagerSpy).toHaveBeenCalledWith( + '/mock/root', + 'run', + ['app'], + expect.objectContaining({ disableRealtimeTelemetry: undefined }) + ); + expect(startServerSpy).toHaveBeenCalled(); + }); + + it('should start manager only when no args are provided', async () => { + start.parseAsync(['node', 'genkit']); + + // Wait a tick for async operations + await new Promise((resolve) => setTimeout(resolve, 10)); + + expect(startManagerSpy).toHaveBeenCalledWith('/mock/root', true); + expect(startDevProcessManagerSpy).not.toHaveBeenCalled(); + expect(startServerSpy).toHaveBeenCalled(); + }); + + it('should not start server if --noui is provided', async () => { + // Cannot await, same reason as above + start.parseAsync(['node', 'genkit', '--noui']); + + await new Promise((resolve) => setTimeout(resolve, 10)); + + expect(startServerSpy).not.toHaveBeenCalled(); + }); + + it('should pass disableRealtimeTelemetry option', async () => { + await start.parseAsync([ + 'node', + 'genkit', + 'run', + 'app', + '--disable-realtime-telemetry', + ]); + + expect(startDevProcessManagerSpy).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + expect.anything(), + expect.objectContaining({ disableRealtimeTelemetry: true }) + ); + }); +}); diff --git a/genkit-tools/cli/tests/utils/manager-utils_test.ts b/genkit-tools/cli/tests/utils/manager-utils_test.ts new file mode 100644 index 0000000000..628556ce56 --- /dev/null +++ b/genkit-tools/cli/tests/utils/manager-utils_test.ts @@ -0,0 +1,100 @@ +/** + * 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. + */ + +import { RuntimeEvent } from '@genkit-ai/tools-common/manager'; +import { beforeEach, describe, expect, it, jest } from '@jest/globals'; +import { waitForRuntime } from '../../src/utils/manager-utils'; + +describe('waitForRuntime', () => { + let mockManager: any; + let mockProcessPromise: Promise; + let processReject: (reason?: any) => void; + + beforeEach(() => { + mockManager = { + getRuntimeById: jest.fn(), + onRuntimeEvent: jest.fn(), + }; + mockProcessPromise = new Promise((_, reject) => { + processReject = reject; + }); + }); + + it('should resolve immediately if runtime is already present', async () => { + mockManager.getRuntimeById.mockReturnValue({}); + await expect( + waitForRuntime(mockManager, 'test-id', mockProcessPromise) + ).resolves.toBeUndefined(); + }); + + it('should wait for runtime event and resolve', async () => { + mockManager.getRuntimeById.mockReturnValue(undefined); + let eventCallback: (event: RuntimeEvent, runtime: any) => void; + + mockManager.onRuntimeEvent.mockImplementation((cb: any) => { + eventCallback = cb; + return jest.fn(); // unsubscribe + }); + + const waitPromise = waitForRuntime( + mockManager, + 'test-id', + mockProcessPromise + ); + + // Simulate event + setTimeout(() => { + eventCallback(RuntimeEvent.ADD, { id: 'test-id' }); + }, 10); + + await expect(waitPromise).resolves.toBeUndefined(); + }); + + it('should reject if process exits early', async () => { + mockManager.getRuntimeById.mockReturnValue(undefined); + mockManager.onRuntimeEvent.mockReturnValue(jest.fn()); + + const waitPromise = waitForRuntime( + mockManager, + 'test-id', + mockProcessPromise + ); + + // Simulate process exit + processReject(new Error('Process exited')); + + await expect(waitPromise).rejects.toThrow('Process exited'); + }); + + it('should timeout if runtime never appears', async () => { + jest.useFakeTimers(); + mockManager.getRuntimeById.mockReturnValue(undefined); + mockManager.onRuntimeEvent.mockReturnValue(jest.fn()); + + const waitPromise = waitForRuntime( + mockManager, + 'test-id', + mockProcessPromise + ); + + jest.advanceTimersByTime(30000); + + await expect(waitPromise).rejects.toThrow( + 'Timeout waiting for runtime to be ready' + ); + jest.useRealTimers(); + }); +}); diff --git a/genkit-tools/common/tests/manager_test.ts b/genkit-tools/common/tests/manager_test.ts new file mode 100644 index 0000000000..c70c38d0a1 --- /dev/null +++ b/genkit-tools/common/tests/manager_test.ts @@ -0,0 +1,47 @@ +/** + * 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. + */ + +import { describe, expect, it, jest } from '@jest/globals'; +import { RuntimeManager } from '../src/manager/manager'; +import { RuntimeEvent } from '../src/manager/types'; + +jest.mock('chokidar', () => ({ + watch: jest.fn().mockReturnValue({ + on: jest.fn(), + close: jest.fn(), + }), +})); + +describe('RuntimeManager', () => { + it('should allow unsubscribing from runtime events', async () => { + const manager = await RuntimeManager.create({ projectRoot: '.' }); + const listener = jest.fn(); + + // Subscribe + const unsubscribe = manager.onRuntimeEvent(listener); + + // Simulate event + (manager as any).eventEmitter.emit(RuntimeEvent.ADD, { id: '1' }); + expect(listener).toHaveBeenCalledTimes(1); + + // Unsubscribe + unsubscribe(); + + // Simulate event again + (manager as any).eventEmitter.emit(RuntimeEvent.ADD, { id: '2' }); + expect(listener).toHaveBeenCalledTimes(1); // Should not have increased + }); +}); From 36197c2e4c8d6cae071d0e81a0b5a3ec8d53e741 Mon Sep 17 00:00:00 2001 From: Samuel Bushi <66321939+ssbushi@users.noreply.github.com> Date: Mon, 12 Jan 2026 12:34:24 -0500 Subject: [PATCH 4/6] Update genkit-tools/common/src/manager/process-manager.ts Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- genkit-tools/common/src/manager/process-manager.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/genkit-tools/common/src/manager/process-manager.ts b/genkit-tools/common/src/manager/process-manager.ts index 308fa10137..7b23e1b414 100644 --- a/genkit-tools/common/src/manager/process-manager.ts +++ b/genkit-tools/common/src/manager/process-manager.ts @@ -143,8 +143,8 @@ export class ProcessManager { this.originalStdIn = undefined; } if (this.appProcess) { - this.appProcess.stdout?.unpipe(); - this.appProcess.stderr?.unpipe(); + this.appProcess.stdout?.removeAllListeners(); + this.appProcess.stderr?.removeAllListeners(); } this.appProcess = undefined; this._status = 'stopped'; From 3329cc5118839facff0fd21e24fa491a0c5ab4da Mon Sep 17 00:00:00 2001 From: Samuel Bushi Date: Thu, 15 Jan 2026 12:25:59 -0500 Subject: [PATCH 5/6] reflection ficx --- js/core/src/reflection.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/js/core/src/reflection.ts b/js/core/src/reflection.ts index 5d6bf6f667..3e314e6d1d 100644 --- a/js/core/src/reflection.ts +++ b/js/core/src/reflection.ts @@ -391,7 +391,13 @@ export class ReflectionServer { `Reflection server (${process.pid}) running on http://localhost:${this.port}` ); ReflectionServer.RUNNING_SERVERS.push(this); - await this.writeRuntimeFile(); + + try { + await this.registry.listActions(); + await this.writeRuntimeFile(); + } catch (e) { + logger.error(`Error initializing plugins: ${e}`); + } }); } From b270c5a55ca49749aeef2998523b454dbb42f4b4 Mon Sep 17 00:00:00 2001 From: Samuel Bushi Date: Thu, 15 Jan 2026 13:11:30 -0500 Subject: [PATCH 6/6] fixes --- genkit-tools/cli/src/utils/manager-utils.ts | 17 +++++----- .../cli/tests/utils/manager-utils_test.ts | 32 ++++++------------- genkit-tools/common/src/manager/manager.ts | 1 + js/core/src/reflection.ts | 7 ++-- 4 files changed, 21 insertions(+), 36 deletions(-) diff --git a/genkit-tools/cli/src/utils/manager-utils.ts b/genkit-tools/cli/src/utils/manager-utils.ts index a7c44062d1..935fe3f19e 100644 --- a/genkit-tools/cli/src/utils/manager-utils.ts +++ b/genkit-tools/cli/src/utils/manager-utils.ts @@ -26,7 +26,6 @@ import { type GenkitToolsError, } from '@genkit-ai/tools-common/manager'; import { logger } from '@genkit-ai/tools-common/utils'; -import * as crypto from 'crypto'; import getPort, { makeRange } from 'get-port'; /** @@ -71,6 +70,7 @@ export async function startManager( export interface DevProcessManagerOptions { disableRealtimeTelemetry?: boolean; nonInteractive?: boolean; + healthCheck?: boolean; } export async function startDevProcessManager( @@ -81,11 +81,9 @@ export async function startDevProcessManager( ): Promise<{ manager: RuntimeManager; processPromise: Promise }> { const telemetryServerUrl = await resolveTelemetryServer(projectRoot); const disableRealtimeTelemetry = options?.disableRealtimeTelemetry ?? false; - const runtimeId = crypto.randomUUID().substring(0, 8); const envVars: Record = { GENKIT_TELEMETRY_SERVER: telemetryServerUrl, GENKIT_ENV: 'dev', - GENKIT_RUNTIME_ID: runtimeId, }; if (!disableRealtimeTelemetry) { envVars.GENKIT_ENABLE_REALTIME_TELEMETRY = 'true'; @@ -100,21 +98,22 @@ export async function startDevProcessManager( }); const processPromise = processManager.start({ ...options, cwd: projectRoot }); - await waitForRuntime(manager, runtimeId, processPromise); + if (options?.healthCheck) { + await waitForRuntime(manager, processPromise); + } return { manager, processPromise }; } /** - * Waits for the runtime with the given ID to register itself. + * Waits for a new runtime to register itself. * Rejects if the process exits or if the timeout is reached. */ export async function waitForRuntime( manager: RuntimeManager, - runtimeId: string, processPromise: Promise ): Promise { - if (manager.getRuntimeById(runtimeId)) { + if (manager.listRuntimes().length > 0) { return; } @@ -132,8 +131,8 @@ export async function waitForRuntime( reject(new Error('Timeout waiting for runtime to be ready')); }, 30000); - unsubscribe = manager.onRuntimeEvent((event, runtime) => { - if (event === RuntimeEvent.ADD && runtime.id === runtimeId) { + unsubscribe = manager.onRuntimeEvent((event) => { + if (event === RuntimeEvent.ADD) { cleanup(); resolve(); } diff --git a/genkit-tools/cli/tests/utils/manager-utils_test.ts b/genkit-tools/cli/tests/utils/manager-utils_test.ts index 628556ce56..bd7bad5f1b 100644 --- a/genkit-tools/cli/tests/utils/manager-utils_test.ts +++ b/genkit-tools/cli/tests/utils/manager-utils_test.ts @@ -25,7 +25,7 @@ describe('waitForRuntime', () => { beforeEach(() => { mockManager = { - getRuntimeById: jest.fn(), + listRuntimes: jest.fn(), onRuntimeEvent: jest.fn(), }; mockProcessPromise = new Promise((_, reject) => { @@ -34,14 +34,14 @@ describe('waitForRuntime', () => { }); it('should resolve immediately if runtime is already present', async () => { - mockManager.getRuntimeById.mockReturnValue({}); + mockManager.listRuntimes.mockReturnValue([{}]); await expect( - waitForRuntime(mockManager, 'test-id', mockProcessPromise) + waitForRuntime(mockManager, mockProcessPromise) ).resolves.toBeUndefined(); }); it('should wait for runtime event and resolve', async () => { - mockManager.getRuntimeById.mockReturnValue(undefined); + mockManager.listRuntimes.mockReturnValue([]); let eventCallback: (event: RuntimeEvent, runtime: any) => void; mockManager.onRuntimeEvent.mockImplementation((cb: any) => { @@ -49,29 +49,21 @@ describe('waitForRuntime', () => { return jest.fn(); // unsubscribe }); - const waitPromise = waitForRuntime( - mockManager, - 'test-id', - mockProcessPromise - ); + const waitPromise = waitForRuntime(mockManager, mockProcessPromise); // Simulate event setTimeout(() => { - eventCallback(RuntimeEvent.ADD, { id: 'test-id' }); + eventCallback(RuntimeEvent.ADD, { id: 'any-id' }); }, 10); await expect(waitPromise).resolves.toBeUndefined(); }); it('should reject if process exits early', async () => { - mockManager.getRuntimeById.mockReturnValue(undefined); + mockManager.listRuntimes.mockReturnValue([]); mockManager.onRuntimeEvent.mockReturnValue(jest.fn()); - const waitPromise = waitForRuntime( - mockManager, - 'test-id', - mockProcessPromise - ); + const waitPromise = waitForRuntime(mockManager, mockProcessPromise); // Simulate process exit processReject(new Error('Process exited')); @@ -81,14 +73,10 @@ describe('waitForRuntime', () => { it('should timeout if runtime never appears', async () => { jest.useFakeTimers(); - mockManager.getRuntimeById.mockReturnValue(undefined); + mockManager.listRuntimes.mockReturnValue([]); mockManager.onRuntimeEvent.mockReturnValue(jest.fn()); - const waitPromise = waitForRuntime( - mockManager, - 'test-id', - mockProcessPromise - ); + const waitPromise = waitForRuntime(mockManager, mockProcessPromise); jest.advanceTimersByTime(30000); diff --git a/genkit-tools/common/src/manager/manager.ts b/genkit-tools/common/src/manager/manager.ts index 1f28d303ad..041e92df44 100644 --- a/genkit-tools/common/src/manager/manager.ts +++ b/genkit-tools/common/src/manager/manager.ts @@ -698,6 +698,7 @@ export class RuntimeManager { runtimeInfo.id ) ) { + console.log('we passed health!'); if ( runtimeInfo.reflectionApiSpecVersion != GENKIT_REFLECTION_API_SPEC_VERSION diff --git a/js/core/src/reflection.ts b/js/core/src/reflection.ts index 3e314e6d1d..7572ec37fa 100644 --- a/js/core/src/reflection.ts +++ b/js/core/src/reflection.ts @@ -103,10 +103,7 @@ export class ReflectionServer { } get runtimeId() { - return ( - process.env.GENKIT_RUNTIME_ID ?? - `${process.pid}${this.port !== null ? `-${this.port}` : ''}` - ); + return `${process.pid}${this.port !== null ? `-${this.port}` : ''}`; } /** @@ -447,7 +444,7 @@ export class ReflectionServer { ); const fileContent = JSON.stringify( { - id: this.runtimeId, + id: process.env.GENKIT_RUNTIME_ID || this.runtimeId, pid: process.pid, name: this.options.name, reflectionServerUrl: `http://localhost:${this.port}`,