firebase-tools
Version:
Command-Line Interface for Firebase
431 lines (430 loc) • 19.5 kB
JavaScript
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;
;