UNPKG

firebase-tools

Version:
431 lines (430 loc) 19.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.promptWriteMode = exports.isPackageJson = exports.isTsConfig = exports.genkitSetup = exports.ensureVertexApiEnabled = exports.doSetup = void 0; const fs = require("fs"); const path = require("path"); const semver = require("semver"); const clc = require("colorette"); const functions_1 = require("../functions"); const prompt_1 = require("../../../prompt"); const spawn_1 = require("../../spawn"); const projectUtils_1 = require("../../../projectUtils"); const ensureApiEnabled_1 = require("../../../ensureApiEnabled"); const logger_1 = require("../../../logger"); const error_1 = require("../../../error"); const utils_1 = require("../../../utils"); const UNKNOWN_VERSION_TOO_HIGH = "2.0.0"; const MIN_VERSION = "0.6.0"; const LATEST_TEMPLATE = "1.0.0"; async function getPackageVersion(packageName, envVariable) { const envVal = process.env[envVariable]; if (envVal && typeof envVal === "string") { if (semver.parse(envVal)) { return envVal; } else { throw new error_1.FirebaseError(`Invalid version string '${envVal}' specified in ${envVariable}`); } } try { const output = await (0, spawn_1.spawnWithOutput)("npm", ["view", packageName, "version"]); if (!output) { throw new error_1.FirebaseError(`Unable to determine ${packageName} version to install`); } return output; } catch (err) { throw new error_1.FirebaseError(`Unable to determine which version of ${packageName} to install.\n` + `npm Error: ${(0, error_1.getErrMsg)(err)}\n\n` + "For a possible workaround run\n npm view " + packageName + " version\n" + "and then set an environment variable:\n" + ` export ${envVariable}=<output from previous command>\n` + "and run `firebase init genkit` again"); } } async function getGenkitInfo() { let templateVersion = LATEST_TEMPLATE; let stopInstall = false; const genkitVersion = await getPackageVersion("genkit", "GENKIT_DEV_VERSION"); const cliVersion = await getPackageVersion("genkit-cli", "GENKIT_CLI_DEV_VERSION"); const vertexVersion = await getPackageVersion("@genkit-ai/vertexai", "GENKIT_VERTEX_VERSION"); const googleAiVersion = await getPackageVersion("@genkit-ai/googleai", "GENKIT_GOOGLEAI_VERSION"); if (semver.gte(genkitVersion, UNKNOWN_VERSION_TOO_HIGH)) { const continueInstall = await (0, prompt_1.confirm)({ message: clc.yellow(`WARNING: The latest version of Genkit (${genkitVersion}) isn't supported by this\n` + "version of firebase-tools. You can proceed, but the provided sample code may\n" + "not work with the latest library. You can also try updating firebase-tools with\n" + "npm install -g firebase-tools@latest, and then running this command again.\n\n") + "Proceed with installing the latest version of Genkit?", default: false, }); if (!continueInstall) { stopInstall = true; } } else if (semver.gte(genkitVersion, "1.0.0-rc.1")) { templateVersion = "1.0.0"; } else if (semver.gte(genkitVersion, MIN_VERSION)) { templateVersion = "0.9.0"; } else { throw new error_1.FirebaseError(`The requested version of Genkit (${genkitVersion}) is no ` + `longer supported. Please specify a newer version.`); } return { genkitVersion, cliVersion, vertexVersion, googleAiVersion, templateVersion, stopInstall, }; } function showStartMessage(setup, command) { logger_1.logger.info(); logger_1.logger.info("\nLogin to Google Cloud using:"); logger_1.logger.info(clc.bold(clc.green(` gcloud auth application-default login --project ${setup.projectId || "your-project-id"}\n`))); logger_1.logger.info("Then start the Genkit developer experience by running:"); logger_1.logger.info(clc.bold(clc.green(` ${command}`))); } async function doSetup(initSetup, config, options) { var _a; const setup = initSetup; const genkitInfo = await getGenkitInfo(); if (genkitInfo.stopInstall) { (0, utils_1.logLabeledWarning)("genkit", "Stopped Genkit initialization"); return; } if (((_a = setup.functions) === null || _a === void 0 ? void 0 : _a.languageChoice) !== "typescript") { const continueFunctions = await (0, prompt_1.confirm)({ message: "Genkit's Firebase integration uses Cloud Functions for Firebase with TypeScript.\nInitialize Functions to continue?", default: true, }); if (!continueFunctions) { (0, utils_1.logLabeledWarning)("genkit", "Stopped Genkit initialization"); return; } setup.languageOverride = "typescript"; await (0, functions_1.doSetup)(setup, config, options); delete setup.languageOverride; logger_1.logger.info(); } if (!setup.functions) { throw new error_1.FirebaseError("Failed to initialize Genkit prerequisite: Firebase functions"); } const projectDir = `${config.projectDir}/${setup.functions.source}`; const installType = await (0, prompt_1.select)({ message: "Install the Genkit CLI globally or locally in this project?", choices: [ { name: "Globally", value: "globally" }, { name: "Just this project", value: "project" }, ], }); try { (0, utils_1.logLabeledBullet)("genkit", `Installing Genkit CLI version ${genkitInfo.cliVersion}`); if (installType === "globally") { await (0, spawn_1.wrapSpawn)("npm", ["install", "-g", `genkit-cli@${genkitInfo.cliVersion}`], projectDir); await genkitSetup(options, genkitInfo, projectDir); showStartMessage(setup, `cd ${setup.functions.source} && npm run genkit:start`); } else { await (0, spawn_1.wrapSpawn)("npm", ["install", `genkit-cli@${genkitInfo.cliVersion}`, "--save-dev"], projectDir); await genkitSetup(options, genkitInfo, projectDir); showStartMessage(setup, `cd ${setup.functions.source} && npm run genkit:start`); } } catch (err) { (0, utils_1.logLabeledError)("genkit", `Genkit initialization failed: ${(0, error_1.getErrMsg)(err)}`); return; } } exports.doSetup = doSetup; async function ensureVertexApiEnabled(options) { const VERTEX_AI_URL = "https://aiplatform.googleapis.com"; const projectId = (0, projectUtils_1.getProjectId)(options); if (!projectId) { return; } const silently = typeof options.markdown === "boolean" && options.markdown; return await (0, ensureApiEnabled_1.ensure)(projectId, VERTEX_AI_URL, "aiplatform", silently); } exports.ensureVertexApiEnabled = ensureVertexApiEnabled; function getModelOptions(genkitInfo) { const modelOptions = { vertexai: { label: "Google Cloud Vertex AI", plugin: "@genkit-ai/vertexai", package: `@genkit-ai/vertexai@${genkitInfo.vertexVersion}`, }, googleai: { label: "Google AI", plugin: "@genkit-ai/googleai", package: `@genkit-ai/googleai@${genkitInfo.googleAiVersion}`, }, none: { label: "None", plugin: undefined, package: undefined }, }; return modelOptions; } const pluginToInfo = { "@genkit-ai/firebase": { imports: "firebase", init: ` // Load the Firebase plugin, which provides integrations with several // Firebase services. firebase()`.trimStart(), }, "@genkit-ai/vertexai": { imports: "vertexAI", modelImportComment: ` // Import models from the Vertex AI plugin. The Vertex AI API provides access to // several generative models. Here, we import Gemini 2.0 Flash.`.trimStart(), init: ` // Load the Vertex AI plugin. You can optionally specify your project ID // by passing in a config object; if you don't, the Vertex AI plugin uses // the value from the GCLOUD_PROJECT environment variable. vertexAI({location: "us-central1"})`.trimStart(), model: "gemini20Flash", }, "@genkit-ai/googleai": { imports: "googleAI", modelImportComment: ` // Import models from the Google AI plugin. The Google AI API provides access to // several generative models. Here, we import Gemini 2.0 Flash.`.trimStart(), init: ` // Load the Google AI plugin. You can optionally specify your API key // by passing in a config object; if you don't, the Google AI plugin uses // the value from the GOOGLE_GENAI_API_KEY environment variable, which is // the recommended practice. googleAI()`.trimStart(), model: "gemini20Flash", }, }; function getBasePackages(genkitVersion) { const basePackages = ["express", `genkit@${genkitVersion}`]; return basePackages; } const externalDevPackages = ["typescript", "tsx"]; async function genkitSetup(options, genkitInfo, projectDir) { var _a, _b; const modelOptions = getModelOptions(genkitInfo); const supportedModels = Object.keys(modelOptions); const model = await (0, prompt_1.select)({ message: "Select a model provider:", choices: supportedModels.map((model) => ({ name: modelOptions[model].label, value: model, })), }); if (model === "vertexai") { await ensureVertexApiEnabled(options); } const plugins = []; const pluginPackages = []; pluginPackages.push(`@genkit-ai/firebase@${genkitInfo.genkitVersion}`); if ((_a = modelOptions[model]) === null || _a === void 0 ? void 0 : _a.plugin) { plugins.push(modelOptions[model].plugin || ""); } if ((_b = modelOptions[model]) === null || _b === void 0 ? void 0 : _b.package) { pluginPackages.push(modelOptions[model].package || ""); } const packages = [...getBasePackages(genkitInfo.genkitVersion)]; packages.push(...pluginPackages); await installNpmPackages(projectDir, packages, externalDevPackages); if (!fs.existsSync(path.join(projectDir, "src"))) { fs.mkdirSync(path.join(projectDir, "src")); } await updateTsConfig(options.nonInteractive || false, projectDir); await updatePackageJson(options.nonInteractive || false, projectDir); if (options.nonInteractive || (await (0, prompt_1.confirm)({ message: "Would you like to generate a sample flow?", default: true, }))) { logger_1.logger.info("Telemetry data can be used to monitor and gain insights into your AI features. There may be a cost associated with using this feature. See https://firebase.google.com/docs/genkit/observability/telemetry-collection."); const enableTelemetry = options.nonInteractive || (await (0, prompt_1.confirm)({ message: "Would like you to enable telemetry collection?", default: true, })); generateSampleFile(modelOptions[model].plugin, plugins, projectDir, genkitInfo.templateVersion, enableTelemetry); } } exports.genkitSetup = genkitSetup; const isTsConfig = (value) => { if (!(0, error_1.isObject)(value) || (value.compilerOptions && !(0, error_1.isObject)(value.compilerOptions))) { return false; } return true; }; exports.isTsConfig = isTsConfig; async function updateTsConfig(nonInteractive, projectDir) { const tsConfigPath = path.join(projectDir, "tsconfig.json"); let existingTsConfig = undefined; if (fs.existsSync(tsConfigPath)) { const parsed = JSON.parse(fs.readFileSync(tsConfigPath, "utf-8")); if (!(0, exports.isTsConfig)(parsed)) { throw new error_1.FirebaseError("Unable to parse existing tsconfig.json"); } existingTsConfig = parsed; } let choice = "overwrite"; if (!nonInteractive && existingTsConfig) { choice = await promptWriteMode("Would you like to update your tsconfig.json with suggested settings?"); } const tsConfig = { compileOnSave: true, include: ["src"], compilerOptions: { module: "commonjs", noImplicitReturns: true, outDir: "lib", sourceMap: true, strict: true, target: "es2017", skipLibCheck: true, esModuleInterop: true, }, }; (0, utils_1.logLabeledBullet)("genkit", "Updating tsconfig.json"); let newTsConfig = {}; switch (choice) { case "overwrite": newTsConfig = Object.assign(Object.assign(Object.assign({}, existingTsConfig), tsConfig), { compilerOptions: Object.assign(Object.assign({}, existingTsConfig === null || existingTsConfig === void 0 ? void 0 : existingTsConfig.compilerOptions), tsConfig.compilerOptions) }); break; case "merge": newTsConfig = Object.assign(Object.assign(Object.assign({}, tsConfig), existingTsConfig), { compilerOptions: Object.assign(Object.assign({}, tsConfig.compilerOptions), existingTsConfig === null || existingTsConfig === void 0 ? void 0 : existingTsConfig.compilerOptions) }); break; case "keep": (0, utils_1.logLabeledWarning)("genkit", "Skipped updating tsconfig.json"); return; } try { fs.writeFileSync(tsConfigPath, JSON.stringify(newTsConfig, null, 2)); (0, utils_1.logLabeledSuccess)("genkit", "Successfully updated tsconfig.json"); } catch (err) { (0, utils_1.logLabeledError)("genkit", `Failed to update tsconfig.json: ${(0, error_1.getErrMsg)(err)}`); process.exit(1); } } async function installNpmPackages(projectDir, packages, devPackages) { (0, utils_1.logLabeledBullet)("genkit", "Installing NPM packages for genkit"); try { if (packages.length) { await (0, spawn_1.wrapSpawn)("npm", ["install", ...packages, "--save"], projectDir); } if (devPackages === null || devPackages === void 0 ? void 0 : devPackages.length) { await (0, spawn_1.wrapSpawn)("npm", ["install", ...devPackages, "--save-dev"], projectDir); } (0, utils_1.logLabeledSuccess)("genkit", "Successfully installed NPM packages"); } catch (err) { (0, utils_1.logLabeledError)("genkit", `Failed to install NPM packages: ${(0, error_1.getErrMsg)(err)}`); process.exit(1); } } function generateSampleFile(modelPlugin, configPlugins, projectDir, templateVersion, enableTelemetry) { let modelImport = ""; if (modelPlugin && pluginToInfo[modelPlugin].model) { const modelInfo = pluginToInfo[modelPlugin].model || ""; modelImport = "\n" + generateImportStatement(modelInfo, modelPlugin) + "\n"; } let modelImportComment = ""; if (modelPlugin && pluginToInfo[modelPlugin].modelImportComment) { const comment = pluginToInfo[modelPlugin].modelImportComment || ""; modelImportComment = `\n${comment}`; } const commentedModelImport = `${modelImportComment}${modelImport}`; const templatePath = path.join(__dirname, `../../../../templates/genkit/firebase.${templateVersion}.template`); const template = fs.readFileSync(templatePath, "utf8"); const sample = renderConfig(configPlugins, template .replace("$GENKIT_MODEL_IMPORT\n", commentedModelImport) .replace("$GENKIT_MODEL", modelPlugin ? pluginToInfo[modelPlugin].model || pluginToInfo[modelPlugin].modelStr || "" : "'' /* TODO: Set a model. */"), enableTelemetry); (0, utils_1.logLabeledBullet)("genkit", "Generating sample file"); try { const samplePath = "src/genkit-sample.ts"; fs.writeFileSync(path.join(projectDir, samplePath), sample, "utf8"); (0, utils_1.logLabeledSuccess)("genkit", `Successfully generated sample file (${samplePath})`); } catch (err) { (0, utils_1.logLabeledError)("genkit", `Failed to generate sample file: ${(0, error_1.getErrMsg)(err)}`); process.exit(1); } } const isPackageJson = (value) => { if (!(0, error_1.isObject)(value) || (value.scripts && !(0, error_1.isObject)(value.scripts))) { return false; } return true; }; exports.isPackageJson = isPackageJson; async function updatePackageJson(nonInteractive, projectDir) { const packageJsonPath = path.join(projectDir, "package.json"); if (!fs.existsSync(packageJsonPath)) { throw new error_1.FirebaseError("Failed to find package.json."); } const existingPackageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")); if (!(0, exports.isPackageJson)(existingPackageJson)) { throw new error_1.FirebaseError("Unable to parse existing package.json file"); } const choice = nonInteractive ? "overwrite" : await promptWriteMode("Would you like to update your package.json with suggested settings?"); const packageJson = { main: "lib/index.js", scripts: { "genkit:start": "genkit start -- tsx --watch src/genkit-sample.ts", }, }; (0, utils_1.logLabeledBullet)("genkit", "Updating package.json"); let newPackageJson = {}; switch (choice) { case "overwrite": newPackageJson = Object.assign(Object.assign(Object.assign({}, existingPackageJson), packageJson), { scripts: Object.assign(Object.assign({}, existingPackageJson.scripts), packageJson.scripts) }); break; case "merge": newPackageJson = Object.assign(Object.assign(Object.assign({}, packageJson), existingPackageJson), { main: packageJson.main, scripts: Object.assign(Object.assign({}, packageJson.scripts), existingPackageJson.scripts) }); break; case "keep": (0, utils_1.logLabeledWarning)("genkit", "Skipped updating package.json"); return; } try { fs.writeFileSync(packageJsonPath, JSON.stringify(newPackageJson, null, 2)); (0, utils_1.logLabeledSuccess)("genkit", "Successfully updated package.json"); } catch (err) { (0, utils_1.logLabeledError)("genkit", `Failed to update package.json: ${(0, error_1.getErrMsg)(err)}`); process.exit(1); } } function renderConfig(pluginNames, template, enableTelemetry) { const imports = pluginNames .map((pluginName) => generateImportStatement(pluginToInfo[pluginName].imports, pluginName)) .join("\n"); const plugins = pluginNames.map((pluginName) => ` ${pluginToInfo[pluginName].init},`).join("\n") || " /* Add your plugins here. */"; return template .replace("$GENKIT_CONFIG_IMPORTS", imports) .replace("$GENKIT_CONFIG_PLUGINS", plugins) .replaceAll("$TELEMETRY_COMMENT", enableTelemetry ? "" : "// "); } function generateImportStatement(imports, name) { return `import {${imports}} from "${name}";`; } async function promptWriteMode(message, defaultOption = "merge") { return (0, prompt_1.select)({ message, choices: [ { name: "Set if unset", value: "merge" }, { name: "Overwrite", value: "overwrite" }, { name: "Keep unchanged", value: "keep" }, ], default: defaultOption, }); } exports.promptWriteMode = promptWriteMode;