Skip to content

feat: Distribute Genkit CLI as executable binaries #2957

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 14 additions & 5 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,15 +105,24 @@ This will build all packages in this repository. This is recommended the first t

### Pack it

Pack packages for testing/distribution.
Pack all packages (libraries, tools, and CLI binaries) for testing/distribution.

Assuming you built everything previously....
Assuming you built everything previously, run the following command from the **project root** directory:

```bash
pnpm run pack:all
```
pnpm pack:all
```

This will produce tarballs in the `dist` folder. Also `genkit-dist.zip` -- a zip of all the package tarballs.
This command will:

1. Produce tarball packages (`.tgz` files) for all workspaces (e.g., from `js/` and `genkit-tools/`) in the project's root `dist/` folder.
2. Produce a `genkit-dist.zip` file (a zip of all the package tarballs) in the project's root `dist/` folder.
3. Additionally, it will use Bun (which must be installed) to create stand-alone executable binaries for the Genkit CLI. These binaries will be located in the `genkit-tools/dist/` folder. Targets include:
- macOS (Apple Silicon): `genkit-bun-macos-arm64`
- macOS (Intel): `genkit-bun-macos-x64`
- Linux (x64): `genkit-bun-linux-x64`
- Windows (x64): `genkit-bun-windows-x64.exe`
These binaries allow users to run the Genkit CLI without needing a Node.js or Bun installation.

### Link it

Expand Down
22 changes: 11 additions & 11 deletions genkit-tools/cli/src/commands/start.ts
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain these changes? They don't seem necessary for packaging the cli as binary.

Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
* limitations under the License.
*/

import { RuntimeManager } from '@genkit-ai/tools-common/manager';
import { startServer } from '@genkit-ai/tools-common/server';
import { logger } from '@genkit-ai/tools-common/utils';
import { spawn } from 'child_process';
Expand All @@ -36,8 +35,9 @@ export const start = new Command('start')
.option('-p, --port <port>', 'port for the Dev UI')
.option('-o, --open', 'Open the browser on UI start up')
.action(async (options: RunOptions) => {
// Always start the manager.
let managerPromise: Promise<RuntimeManager> = startManager(true);
// Always start the manager first.
const manager = await startManager(true);

if (!options.noui) {
let port: number;
if (options.port) {
Expand All @@ -49,18 +49,18 @@ export const start = new Command('start')
} else {
port = await getPort({ port: makeRange(4000, 4099) });
}
managerPromise = managerPromise.then((manager) => {
startServer(manager, port);
return manager;
});
// Await the server startup completely.
// startServer is async and internally calls writeToolsInfoFile.
await startServer(manager, port);
if (options.open) {
open(`http://localhost:${port}`);
}
}
await managerPromise.then((manager: RuntimeManager) => {
const telemetryServerUrl = manager?.telemetryServerUrl;
return startRuntime(telemetryServerUrl);
});

// Now that the manager is initialized and the UI server (if enabled) has started
// (and should have written its tools file), proceed to start the runtime.
const telemetryServerUrl = manager?.telemetryServerUrl;
await startRuntime(telemetryServerUrl);
});

async function startRuntime(telemetryServerUrl?: string) {
Expand Down
8 changes: 7 additions & 1 deletion genkit-tools/common/src/manager/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,12 @@ export class RuntimeManager {
const watcher = chokidar.watch(serversDir, {
persistent: true,
ignoreInitial: false,
usePolling: true, // Force polling
interval: 400, // Poll every 400ms
awaitWriteFinish: {
stabilityThreshold: 3000, // Wait 3s after last write to consider it stable
pollInterval: 100, // Check stability every 100ms (for awaitWriteFinish)
},
});
watcher.on('add', (filePath) => this.handleNewDevUi(filePath));
if (this.manageHealth) {
Expand All @@ -385,7 +391,7 @@ export class RuntimeManager {
const toolsInfo = JSON.parse(content) as DevToolsInfo;
return { content, toolsInfo };
},
{ maxRetries: 10, delayMs: 500 }
{ maxRetries: 20, delayMs: 750 } // Increased retries and delay
);

if (isValidDevToolsInfo(toolsInfo)) {
Expand Down
4 changes: 2 additions & 2 deletions genkit-tools/common/src/server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,12 @@ const API_BASE_PATH = '/api';
/**
* Starts up the Genkit Tools server which includes static files for the UI and the Tools API.
*/
export function startServer(manager: RuntimeManager, port: number) {
export async function startServer(manager: RuntimeManager, port: number) {
let server: Server;
const app = express();

// Download UI assets from public GCS bucket and serve locally
downloadAndExtractUiAssets({
await downloadAndExtractUiAssets({
fileUrl: UI_ASSETS_ZIP_GCS_PATH,
extractPath: UI_ASSETS_ROOT,
zipFileName: UI_ASSETS_ZIP_FILE_NAME,
Expand Down
23 changes: 22 additions & 1 deletion genkit-tools/common/src/utils/package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,25 @@ import { readFileSync } from 'fs';
import { join } from 'path';

const packagePath = join(__dirname, '../../../package.json');
export const toolsPackage = JSON.parse(readFileSync(packagePath, 'utf8'));

interface MinimalPackageJson {
name: string;
version: string;
[key: string]: any;
}

// Attempt to read package.json, with a fallback for safety,
// though Bun's bundling should make this reliable.
let pkg: MinimalPackageJson;
try {
pkg = JSON.parse(readFileSync(packagePath, 'utf8')) as MinimalPackageJson;
} catch (e) {
// This fallback should ideally not be hit if Bun bundles the package.json correctly.
// Logging a warning if it does get hit during development or in a strange environment.
console.warn(
`[genkit-tools-common] Warning: Could not read package.json at '${packagePath}'. Using fallback values. Error: ${e}`
);
pkg = { name: 'genkit-tools', version: '0.0.0-fallback' };
}

export const toolsPackage = pkg;
31 changes: 25 additions & 6 deletions genkit-tools/common/src/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,15 +144,34 @@ export async function checkServerHealth(url: string): Promise<boolean> {
try {
const response = await fetch(`${url}/api/__health`);
return response.status === 200;
} catch (error) {
} catch (error: any) {
// Catch as 'any' for broader inspection
// Log the actual error received during health check for debugging
logger.debug(
`Health check for ${url} failed. Error type: ${typeof error}, Error:`,
error
);

// Check for ECONNREFUSED more safely
if (
error instanceof Error &&
error &&
error.cause &&
typeof error.cause === 'object' &&
(error.cause as any).code === 'ECONNREFUSED'
) {
return false;
}
// If it's any other error during fetch, also consider it unhealthy for this check's purpose
// or if it's not an Error instance with a cause.
// The original code would return `true` here, which seems wrong.
// If fetch fails for *any* reason, the server is not healthy from this check's POV.
return false;
}
return true;
// This line should not be reached if the try block has a return or the catch block has a return.
// However, to satisfy TypeScript if it thinks not all paths return, and as a fallback:
// But logically, if fetch succeeds, it returns from try. If it fails, it returns from catch.
// The original code had `return true` here, which would mean if an error (not ECONNREFUSED) occurred, it was considered healthy.
// Let's stick to: if fetch fails, it's false.
}

/**
Expand Down Expand Up @@ -210,17 +229,17 @@ export async function retriable<T>(

let attempt = 0;
while (true) {
attempt++;
try {
return await fn();
} catch (e) {
if (attempt >= maxRetries - 1) {
if (attempt >= maxRetries) {
throw e;
}
if (delayMs > 0) {
await new Promise((r) => setTimeout(r, delayMs));
}
}
attempt++;
}
}

Expand Down Expand Up @@ -250,7 +269,7 @@ export async function writeToolsInfoFile(url: string, projectRoot?: string) {
await fs.writeFile(toolsJsonPath, JSON.stringify(serverInfo, null, 2));
logger.debug(`Tools Info file written: ${toolsJsonPath}`);
} catch (error) {
logger.info('Error writing tools config', error);
logger.info('Error writing tools config file:', error);
}
}

Expand Down
3 changes: 2 additions & 1 deletion genkit-tools/common/tsconfig.esm.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./lib/esm",
"module": "esnext"
"module": "esnext",
"resolveJsonModule": true
}
}
3 changes: 2 additions & 1 deletion genkit-tools/common/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"module": "ES2022",
"module": "esnext",
"moduleResolution": "Node",
"outDir": "lib/esm",
"esModuleInterop": true,
"resolveJsonModule": true,
"typeRoots": ["./node_modules/@types"],
"rootDirs": ["src"]
},
Expand Down
3 changes: 2 additions & 1 deletion genkit-tools/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"pack:all": "pnpm run pack:cli && pnpm run pack:telemetry-server && pnpm run pack:common",
"pack:common": "cd common && pnpm pack --pack-destination ../../dist",
"pack:cli": "cd cli && pnpm pack --pack-destination ../../dist",
"pack:telemetry-server": "cd telemetry-server && pnpm pack --pack-destination ../../dist"
"pack:telemetry-server": "cd telemetry-server && pnpm pack --pack-destination ../../dist",
"package:binaries": "pnpm run build && echo \"Building for macOS (Apple Silicon, arm64)...\" && bun build cli/src/bin/genkit.ts --compile --outfile dist/genkit-bun-macos-arm64 --target bun-darwin-arm64 && echo \"Building for macOS (Intel, x64)...\" && bun build cli/src/bin/genkit.ts --compile --outfile dist/genkit-bun-macos-x64 --target bun-darwin-x64 && echo \"Building for Linux (x64)...\" && bun build cli/src/bin/genkit.ts --compile --outfile dist/genkit-bun-linux-x64 --target bun-linux-x64 && echo \"Building for Windows (x64)...\" && bun build cli/src/bin/genkit.ts --compile --outfile dist/genkit-bun-windows-x64.exe --target bun-windows-x64 && echo \"Binary packaging complete.\""
},
"devDependencies": {
"json-schema": "^0.4.0",
Expand Down
6 changes: 3 additions & 3 deletions genkit-tools/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading