Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
- Added 'emulators' to `firebase_init` MCP tool.(##9200)
- Deprecated Java versions below 21. Support will be dropped in Firebase CLI v15. Please upgrade to Java version 21 or above to continue using the emulators.
- Fix Functions MCP log tool to normalize sort order and surface Cloud Logging error details (#9247)
- Fixed an issue where `firebase init` would require log in even when no project is selected. (#9251)
109 changes: 71 additions & 38 deletions src/init/features/emulators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,105 +9,138 @@
import { Config } from "../../config";
import { EmulatorsConfig } from "../../firebaseConfig";

interface EmulatorsInitSelections {
emulators?: Emulators[];
download?: boolean;
export interface RequiredInfo {
emulators: Emulators[];
download: boolean;
config: EmulatorsConfig;
}

export async function doSetup(setup: Setup, config: Config) {
export async function askQuestions(setup: Setup, config: Config): Promise<void> {

Check warning on line 18 in src/init/features/emulators.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing JSDoc comment
const choices = ALL_SERVICE_EMULATORS.map((e) => {
return {
value: e,
// TODO: latest versions of inquirer have a name vs description.
// We should learn more and whether it's worth investing in.
name: Constants.description(e),
checked: config?.has(e) || config?.has(`emulators.${e}`),
};
});

const selections: EmulatorsInitSelections = {};
selections.emulators = await checkbox<Emulators>({
const selectedEmulators = await checkbox<Emulators>({
message:
"Which Firebase emulators do you want to set up? " +
"Press Space to select emulators, then Enter to confirm your choices.",
choices: choices,
});

if (!selections.emulators) {
if (!selectedEmulators || !selectedEmulators.length) {
return;
}

setup.config.emulators = setup.config.emulators || {};
const emulators: EmulatorsConfig = setup.config.emulators || {};
for (const selected of selections.emulators) {
if (selected === "extensions") continue;
const selectedEmulator = emulators[selected] || {};
setup.featureInfo = setup.featureInfo || {};
const emulatorsInfo: RequiredInfo = {
emulators: selectedEmulators,
config: {},
download: false,
};
setup.featureInfo.emulators = emulatorsInfo;

const newConfig = emulatorsInfo.config;
const existingConfig = setup.config.emulators || {};

const currentPort = selectedEmulator.port;
for (const selected of selectedEmulators) {
if (selected === "extensions") continue;
newConfig[selected] = {};
const currentPort = existingConfig[selected]?.port;
if (currentPort) {
utils.logBullet(`Port for ${selected} already configured: ${clc.cyan(currentPort)}`);
} else {
selectedEmulator.port = await number({
newConfig[selected]!.port = await number({

Check warning on line 56 in src/init/features/emulators.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Forbidden non-null assertion
message: `Which port do you want to use for the ${clc.underline(selected)} emulator?`,
default: Constants.getDefaultPort(selected),
});
}
emulators[selected] = selectedEmulator;

const additionalInitFn = AdditionalInitFns[selected];
if (additionalInitFn) {
const additionalOptions = await additionalInitFn(config);
if (additionalOptions) {
emulators[selected] = {
...setup.config.emulators[selected],
...additionalOptions,
};
Object.assign(newConfig[selected]!, additionalOptions);

Check warning on line 66 in src/init/features/emulators.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Forbidden non-null assertion
}
}
}

if (selections.emulators.length) {
if (selectedEmulators.length) {
const uiDesc = Constants.description(Emulators.UI);
if (setup.config.emulators.ui && setup.config.emulators.ui.enabled !== false) {
const currentPort = setup.config.emulators.ui.port || "(automatic)";
utils.logBullet(`${uiDesc} already enabled with port: ${clc.cyan(currentPort)}`);
} else {
const ui = setup.config.emulators.ui || {};
setup.config.emulators.ui = ui;
const existingUiConfig = existingConfig.ui || {};
newConfig.ui = {};

ui.enabled = await confirm({
let enableUi: boolean;
if (existingUiConfig.enabled !== undefined) {
utils.logBullet(`${uiDesc} already ${existingUiConfig.enabled ? "enabled" : "disabled"}.`);
enableUi = existingUiConfig.enabled;
} else {
enableUi = await confirm({
message: `Would you like to enable the ${uiDesc}?`,
default: true,
});
}
newConfig.ui.enabled = enableUi;

if (ui.enabled) {
ui.port = await number({
if (newConfig.ui.enabled) {
const currentPort = existingUiConfig.port;
if (currentPort) {
utils.logBullet(`Port for ${uiDesc} already configured: ${clc.cyan(currentPort)}`);
} else {
newConfig.ui.port = await number({
message: `Which port do you want to use for the ${clc.underline(uiDesc)} (leave empty to use any available port)?`,
required: false,
});
}
}
}

selections.download = await confirm({
if (selectedEmulators.length) {
emulatorsInfo.download = await confirm({
message: "Would you like to download the emulators now?",
default: true,
});
}
}

export async function actuate(setup: Setup): Promise<void> {

Check warning on line 109 in src/init/features/emulators.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing JSDoc comment
const emulatorsInfo = setup.featureInfo?.emulators;
if (!emulatorsInfo) {
return;
}

setup.config.emulators = setup.config.emulators || {};
const emulatorsConfig = setup.config.emulators;

// Merge the config from the questions into the main config.
for (const emulatorName of Object.keys(emulatorsInfo.config)) {
const key = emulatorName as keyof EmulatorsConfig;
if (key === "ui") {
emulatorsConfig.ui = { ...emulatorsConfig.ui, ...emulatorsInfo.config.ui };
} else if (key === "singleProjectMode") {
emulatorsConfig.singleProjectMode = emulatorsInfo.config[key];
} else if (emulatorsInfo.config[key]) {
emulatorsConfig[key] = { ...emulatorsConfig[key], ...emulatorsInfo.config[key] };
}
}

// Set the default behavior to be single project mode.
if (setup.config.emulators.singleProjectMode === undefined) {
setup.config.emulators.singleProjectMode = true;
if (emulatorsConfig.singleProjectMode === undefined) {
emulatorsConfig.singleProjectMode = true;
}

if (selections.download) {
for (const selected of selections.emulators) {
if (emulatorsInfo.download) {
for (const selected of emulatorsInfo.emulators) {
if (isDownloadableEmulator(selected)) {
await downloadIfNecessary(selected);
}
}

if (setup?.config?.emulators?.ui?.enabled) {
downloadIfNecessary(Emulators.UI);
if (emulatorsConfig.ui?.enabled) {
await downloadIfNecessary(Emulators.UI);
}
}
}
6 changes: 5 additions & 1 deletion src/init/features/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ export {
RequiredInfo as StorageInfo,
actuate as storageActuate,
} from "./storage";
export { doSetup as emulators } from "./emulators";
export {
askQuestions as emulatorsAskQuestions,
RequiredInfo as EmulatorsInfo,
actuate as emulatorsActuate,
} from "./emulators";
export { doSetup as extensions } from "./extensions";
// always runs, sets up .firebaserc
export { doSetup as project } from "./project";
Expand Down
7 changes: 6 additions & 1 deletion src/init/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@
instructions: string[];

/** Basic Project information */
project?: Record<string, any>;

Check warning on line 26 in src/init/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type
projectId?: string;
projectLocation?: string;
isBillingEnabled?: boolean;

hosting?: Record<string, any>;

Check warning on line 31 in src/init/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type
}

export interface SetupInfo {
Expand All @@ -38,6 +38,7 @@
dataconnectSdk?: features.DataconnectSdkInfo;
storage?: features.StorageInfo;
apptesting?: features.ApptestingInfo;
emulators?: features.EmulatorsInfo;
ailogic?: features.AiLogicInfo;
}

Expand Down Expand Up @@ -85,7 +86,11 @@
askQuestions: features.storageAskQuestions,
actuate: features.storageActuate,
},
{ name: "emulators", doSetup: features.emulators },
{
name: "emulators",
askQuestions: features.emulatorsAskQuestions,
actuate: features.emulatorsActuate,
},
{ name: "extensions", doSetup: features.extensions },
{ name: "project", doSetup: features.project }, // always runs, sets up .firebaserc
{ name: "remoteconfig", doSetup: features.remoteconfig },
Expand All @@ -107,7 +112,7 @@

const featureMap = new Map(featuresList.map((feature) => [feature.name, feature]));

export async function init(setup: Setup, config: Config, options: any): Promise<any> {

Check warning on line 115 in src/init/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type

Check warning on line 115 in src/init/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type

Check warning on line 115 in src/init/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing JSDoc comment
const nextFeature = setup.features?.shift();
if (nextFeature) {
const start = process.uptime();
Expand All @@ -127,7 +132,7 @@
);

if (f.doSetup) {
await f.doSetup(setup, config, options);

Check warning on line 135 in src/init/index.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe argument of type `any` assigned to a parameter of type `Options`
} else {
if (f.askQuestions) {
await f.askQuestions(setup, config, options);
Expand Down
60 changes: 57 additions & 3 deletions src/mcp/tools/core/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,24 @@ import { DEFAULT_RULES } from "../../../init/features/database";
import { actuate, Setup, SetupInfo } from "../../../init/index";
import { freeTrialTermsLink } from "../../../dataconnect/freeTrial";
import { requireGeminiToS } from "../../errors";
import { Emulators } from "../../../emulator/types";
import { FirebaseError } from "../../../error";
import { getFirebaseProject } from "../../../management/projects";
import {
parseAppId,
validateProjectNumberMatch,
validateAppExists,
} from "../../../init/features/ailogic/utils";
import { getFirebaseProject } from "../../../management/projects";
const emulatorHostPortSchema = z.object({
host: z.string().optional().describe("The host to use for the emulator."),
port: z.number().optional().describe("The port to use for the emulator."),
});

export const init = tool(
{
name: "init",
description:
"Use this to initialize selected Firebase services in the workspace (Cloud Firestore database, Firebase Data Connect, Firebase Realtime Database, Firebase AI Logic). All services are optional; specify only the products you want to set up. " +
"Use this to initialize selected Firebase services in the workspace (Cloud Firestore database, Firebase Data Connect, Firebase Realtime Database, Firebase AI Logic, Emulators). All services are optional; specify only the products you want to set up. " +
"You can initialize new features into an existing project directory, but re-initializing an existing feature may overwrite configuration. " +
"To deploy the initialized features, run the `firebase deploy` command after `firebase_init` tool.",
inputSchema: z.object({
Expand Down Expand Up @@ -128,6 +133,31 @@ export const init = tool(
.describe(
"Provide this object to initialize Firebase Storage in this project directory.",
),
emulators: z
.object({
auth: emulatorHostPortSchema.optional(),
database: emulatorHostPortSchema.optional(),
firestore: emulatorHostPortSchema.optional(),
functions: emulatorHostPortSchema.optional(),
hosting: emulatorHostPortSchema.optional(),
storage: emulatorHostPortSchema.optional(),
pubsub: emulatorHostPortSchema.optional(),
ui: z
.object({
enabled: z.boolean().optional(),
host: z.string().optional(),
port: z.number().optional(),
})
.optional(),
singleProjectMode: z
.boolean()
.optional()
.describe("If true, do not warn on detection of multiple project IDs."),
})
.optional()
.describe(
"Provide this object to configure Firebase emulators in this project directory.",
),
ailogic: z
.object({
app_id: z
Expand Down Expand Up @@ -195,6 +225,30 @@ export const init = tool(
apps: [],
};
}
if (features.storage) {
featuresList.push("storage");
featureInfo.storage = {
rulesFilename: features.storage.rules_filename,
rules: features.storage.rules || "",
writeRules: true,
};
}
if (features.emulators) {
featuresList.push("emulators");
const emulatorKeys = Object.keys(features.emulators).filter(
(key) =>
key !== "ui" &&
key !== "singleProjectMode" &&
Object.values(Emulators).includes(key as Emulators),
) as Emulators[];

featureInfo.emulators = {
emulators: emulatorKeys,
config: features.emulators,
download: true, // Non-interactive, so default to downloading.
};
}

if (features.ailogic) {
// AI Logic requires a project
if (!projectId) {
Expand All @@ -217,7 +271,7 @@ export const init = tool(
};
}
const setup: Setup = {
config: config?.src,
config: config?.src || {},
rcfile: rc?.data,
projectId: projectId,
features: [...featuresList],
Expand Down
Loading