diff --git a/CHANGELOG.md b/CHANGELOG.md
index 45060095ac8..44dcbdd780e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1 +1,2 @@
- 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)
diff --git a/firebase-vscode/CHANGELOG.md b/firebase-vscode/CHANGELOG.md
index a688cf5bb98..23afe5f27f8 100644
--- a/firebase-vscode/CHANGELOG.md
+++ b/firebase-vscode/CHANGELOG.md
@@ -1,6 +1,7 @@
## NEXT
- [Added] Refine / Generate Operation Code Lens.
+- [Added] Support run "firebase init" without login and project.
## 1.8.0
diff --git a/firebase-vscode/src/analytics.ts b/firebase-vscode/src/analytics.ts
index 20c5aba4fb7..b65d8a0114f 100644
--- a/firebase-vscode/src/analytics.ts
+++ b/firebase-vscode/src/analytics.ts
@@ -27,6 +27,7 @@ export enum DATA_CONNECT_EVENT_NAME {
MOVE_TO_CONNECTOR = "move_to_connector",
START_EMULATOR_FROM_EXECUTION = "start_emulator_from_execution",
REFUSE_START_EMULATOR_FROM_EXECUTION = "refuse_start_emulator_from_execution",
+ INIT = "init",
INIT_SDK = "init_sdk",
INIT_SDK_CLI = "init_sdk_cli",
INIT_SDK_CODELENSE = "init_sdk_codelense",
diff --git a/firebase-vscode/src/core/index.ts b/firebase-vscode/src/core/index.ts
index 1b70508edd1..4032ecf5286 100644
--- a/firebase-vscode/src/core/index.ts
+++ b/firebase-vscode/src/core/index.ts
@@ -14,7 +14,8 @@ import { upsertFile } from "../data-connect/file-utils";
import { registerWebhooks } from "./webhook";
import { createE2eMockable } from "../utils/test_hooks";
import { runTerminalTask } from "../data-connect/terminal";
-import { AnalyticsLogger } from "../analytics";
+import { AnalyticsLogger, DATA_CONNECT_EVENT_NAME } from "../analytics";
+import { EmulatorHub } from "../../../src/emulator/hub";
export async function registerCore(
broker: ExtensionBrokerImpl,
@@ -66,10 +67,9 @@ export async function registerCore(
);
return;
}
- const initCommand = currentProjectId.value
- ? `${settings.firebasePath} init dataconnect --project ${currentProjectId.value}`
- : `${settings.firebasePath} init dataconnect`;
-
+ analyticsLogger.logger.logUsage(DATA_CONNECT_EVENT_NAME.INIT);
+ const projectId = currentProjectId.value || EmulatorHub.MISSING_PROJECT_PLACEHOLDER;
+ const initCommand = `${settings.firebasePath} init dataconnect --project ${projectId}`;
initSpy.call("firebase init", initCommand, { focus: true });
});
diff --git a/firebase-vscode/webviews/SidebarApp.tsx b/firebase-vscode/webviews/SidebarApp.tsx
index 16389d4e17a..318a34f7f95 100644
--- a/firebase-vscode/webviews/SidebarApp.tsx
+++ b/firebase-vscode/webviews/SidebarApp.tsx
@@ -277,14 +277,13 @@ export function SidebarApp() {
- {user.value &&
- (isInitialized.value ? (
-
- ) : (
-
-
-
- ))}
+ {isInitialized.value ? (
+
+ ) : (
+
+
+
+ )}
);
}
diff --git a/src/commands/init.ts b/src/commands/init.ts
index 8186241d498..39409efcac6 100644
--- a/src/commands/init.ts
+++ b/src/commands/init.ts
@@ -8,7 +8,6 @@ import { getAllAccounts } from "../auth";
import { init, Setup } from "../init";
import { logger } from "../logger";
import { checkbox, confirm } from "../prompt";
-import { requireAuth } from "../requireAuth";
import * as fsutils from "../fsutils";
import * as utils from "../utils";
import { Options } from "../options";
@@ -147,7 +146,6 @@ ${[...featureNames]
export const command = new Command("init [feature]")
.description("interactively configure the current directory as a Firebase project directory")
.help(HELP)
- .before(requireAuth)
.action(initAction);
/**
diff --git a/src/init/features/project.spec.ts b/src/init/features/project.spec.ts
index 15a305d3c8e..868a5e6afc9 100644
--- a/src/init/features/project.spec.ts
+++ b/src/init/features/project.spec.ts
@@ -8,6 +8,7 @@ import * as projectManager from "../../management/projects";
import { Config } from "../../config";
import { FirebaseProjectMetadata } from "../../types/project";
import * as promptImport from "../../prompt";
+import * as requireAuthImport from "../../requireAuth";
const TEST_FIREBASE_PROJECT: FirebaseProjectMetadata = {
projectId: "my-project-123",
@@ -34,6 +35,7 @@ describe("project", () => {
let emptyConfig: Config;
beforeEach(() => {
+ sandbox.stub(requireAuthImport, "requireAuth").resolves();
getProjectStub = sandbox.stub(projectManager, "getFirebaseProject");
createFirebaseProjectStub = sandbox.stub(projectManager, "createFirebaseProjectAndLog");
getOrPromptProjectStub = sandbox.stub(projectManager, "getOrPromptProject");
@@ -94,26 +96,6 @@ describe("project", () => {
displayName: "my-project",
});
});
-
- it("should throw if project ID is empty after prompt", async () => {
- const options = {};
- const setup = { config: {}, rcfile: {} };
- prompt.select.onFirstCall().resolves("Create a new project");
- prompt.input.resolves("");
- configstoreSetStub.onFirstCall().resolves();
-
- let err;
- try {
- await doSetup(setup, emptyConfig, options);
- } catch (e: any) {
- err = e;
- }
-
- expect(err.message).to.equal("Project ID cannot be empty");
- expect(prompt.select).to.be.calledOnce;
- expect(prompt.input).to.be.calledTwice;
- expect(createFirebaseProjectStub).to.be.not.called;
- });
});
describe('with "Add Firebase resources to GCP project" option', () => {
@@ -137,27 +119,6 @@ describe("project", () => {
expect(promptAvailableProjectIdStub).to.be.calledOnce;
expect(addFirebaseProjectStub).to.be.calledOnceWith("my-project-123");
});
-
- it("should throw if project ID is empty after prompt", async () => {
- const options = {};
- const setup = { config: {}, rcfile: {} };
- prompt.select
- .onFirstCall()
- .resolves("Add Firebase to an existing Google Cloud Platform project");
- promptAvailableProjectIdStub.onFirstCall().resolves("");
-
- let err;
- try {
- await doSetup(setup, emptyConfig, options);
- } catch (e: any) {
- err = e;
- }
-
- expect(err.message).to.equal("Project ID cannot be empty");
- expect(prompt.select).to.be.calledOnce;
- expect(promptAvailableProjectIdStub).to.be.calledOnce;
- expect(addFirebaseProjectStub).to.be.not.called;
- });
});
describe(`with "Don't set up a default project" option`, () => {
@@ -181,6 +142,7 @@ describe("project", () => {
options = {};
setup = { config: {}, rcfile: { projects: { default: "my-project-123" } } };
getProjectStub.onFirstCall().resolves(TEST_FIREBASE_PROJECT);
+ configstoreSetStub.onFirstCall().resolves();
});
it("should not prompt", async () => {
diff --git a/src/init/features/project.ts b/src/init/features/project.ts
index 762921108e9..1e7b6527f57 100644
--- a/src/init/features/project.ts
+++ b/src/init/features/project.ts
@@ -1,97 +1,27 @@
import * as clc from "colorette";
import * as _ from "lodash";
-import { FirebaseError } from "../../error";
import {
addFirebaseToCloudProjectAndLog,
createFirebaseProjectAndLog,
getFirebaseProject,
- getOrPromptProject,
promptAvailableProjectId,
promptProjectCreation,
+ selectProjectInteractively,
} from "../../management/projects";
import { FirebaseProjectMetadata } from "../../types/project";
import { logger } from "../../logger";
import * as utils from "../../utils";
import * as prompt from "../../prompt";
-import { Options } from "../../options";
+import { requireAuth } from "../../requireAuth";
+import { Constants } from "../../emulator/constants";
+import { FirebaseError } from "../../error";
const OPTION_NO_PROJECT = "Don't set up a default project";
const OPTION_USE_PROJECT = "Use an existing project";
const OPTION_NEW_PROJECT = "Create a new project";
const OPTION_ADD_FIREBASE = "Add Firebase to an existing Google Cloud Platform project";
-/**
- * Used in init flows to keep information about the project - basically
- * a shorter version of {@link FirebaseProjectMetadata} with some additional fields.
- */
-export interface InitProjectInfo {
- id: string; // maps to FirebaseProjectMetadata.projectId
- label?: string;
- instance?: string; // maps to FirebaseProjectMetadata.resources.realtimeDatabaseInstance
- location?: string; // maps to FirebaseProjectMetadata.resources.locationId
-}
-
-function toInitProjectInfo(projectMetaData: FirebaseProjectMetadata): InitProjectInfo {
- const { projectId, displayName, resources } = projectMetaData;
- return {
- id: projectId,
- label: `${projectId}` + (displayName ? ` (${displayName})` : ""),
- instance: resources?.realtimeDatabaseInstance,
- location: resources?.locationId,
- };
-}
-
-async function promptAndCreateNewProject(options: Options): Promise {
- utils.logBullet(
- "If you want to create a project in a Google Cloud organization or folder, please use " +
- `"firebase projects:create" instead, and return to this command when you've created the project.`,
- );
- const { projectId, displayName } = await promptProjectCreation(options);
- // N.B. This shouldn't be possible because of the validator on the input field, but it
- // is being left around in case there's something I don't know.
- if (!projectId) {
- throw new FirebaseError("Project ID cannot be empty");
- }
-
- return await createFirebaseProjectAndLog(projectId, { displayName });
-}
-
-async function promptAndAddFirebaseToCloudProject(): Promise {
- const projectId = await promptAvailableProjectId();
- if (!projectId) {
- // N.B. This shouldn't be possible because of the validator on the input field, but it
- // is being left around in case there's something I don't know.
- throw new FirebaseError("Project ID cannot be empty");
- }
- return await addFirebaseToCloudProjectAndLog(projectId);
-}
-
-/**
- * Prompt the user about how they would like to select a project.
- * @param options the Firebase CLI options object.
- * @return the project metadata, or undefined if no project was selected.
- */
-async function projectChoicePrompt(options: any): Promise {
- const choices = [OPTION_USE_PROJECT, OPTION_NEW_PROJECT, OPTION_ADD_FIREBASE, OPTION_NO_PROJECT];
- const projectSetupOption: string = await prompt.select<(typeof choices)[number]>({
- message: "Please select an option:",
- choices,
- });
-
- switch (projectSetupOption) {
- case OPTION_USE_PROJECT:
- return getOrPromptProject(options);
- case OPTION_NEW_PROJECT:
- return promptAndCreateNewProject(options);
- case OPTION_ADD_FIREBASE:
- return promptAndAddFirebaseToCloudProject();
- default:
- // Do nothing if user chooses NO_PROJECT
- return;
- }
-}
-
/**
* Sets up the default project if provided and writes .firebaserc file.
* @param setup A helper object to use for the rest of the init features.
@@ -106,52 +36,94 @@ export async function doSetup(setup: any, config: any, options: any): Promise({
+ message: "Please select an option:",
+ choices,
+ });
+ switch (projectSetupOption) {
+ case OPTION_USE_PROJECT: {
+ await requireAuth(options);
+ const pm = await selectProjectInteractively();
+ return await usingProjectMetadata(setup, config, pm);
+ }
+ case OPTION_NEW_PROJECT: {
+ utils.logBullet(
+ "If you want to create a project in a Google Cloud organization or folder, please use " +
+ `"firebase projects:create" instead, and return to this command when you've created the project.`,
+ );
+ await requireAuth(options);
+ const { projectId, displayName } = await promptProjectCreation(options);
+ const pm = await createFirebaseProjectAndLog(projectId, { displayName });
+ return await usingProjectMetadata(setup, config, pm);
+ }
+ case OPTION_ADD_FIREBASE: {
+ await requireAuth(options);
+ const pm = await addFirebaseToCloudProjectAndLog(await promptAvailableProjectId());
+ return await usingProjectMetadata(setup, config, pm);
}
+ default:
+ // Do nothing if user chooses NO_PROJECT
+ return;
}
+}
- const projectInfo = toInitProjectInfo(projectMetaData);
- utils.logBullet(`Using project ${projectInfo.label}`);
+async function usingProject(
+ setup: any,
+ config: any,
+ projectId: string,
+ from: string = "",
+): Promise {
+ const pm = await getFirebaseProject(projectId);
+ const label = `${pm.projectId}` + (pm.displayName ? ` (${pm.displayName})` : "");
+ utils.logBullet(`Using project ${label} ${from ? "from ${from}" : ""}.`);
+ await usingProjectMetadata(setup, config, pm);
+}
+
+async function usingProjectMetadata(
+ setup: any,
+ config: any,
+ pm: FirebaseProjectMetadata,
+): Promise {
+ if (!pm) {
+ throw new FirebaseError("null FirebaseProjectMetadata");
+ }
// write "default" alias and activate it immediately
- _.set(setup.rcfile, "projects.default", projectInfo.id);
- setup.projectId = projectInfo.id;
- setup.instance = projectInfo.instance;
- setup.projectLocation = projectInfo.location;
- utils.makeActiveProject(config.projectDir, projectInfo.id);
+ _.set(setup.rcfile, "projects.default", pm.projectId);
+ setup.projectId = pm.projectId;
+ setup.instance = pm.resources?.realtimeDatabaseInstance;
+ setup.projectLocation = pm.resources?.locationId;
+ utils.makeActiveProject(config.projectDir, pm.projectId);
}
diff --git a/src/management/projects.ts b/src/management/projects.ts
index a45ea60fd09..65b465aa150 100644
--- a/src/management/projects.ts
+++ b/src/management/projects.ts
@@ -11,6 +11,7 @@ import * as utils from "../utils";
import { FirebaseProjectMetadata, CloudProjectInfo, ProjectPage } from "../types/project";
import { bestEffortEnsure } from "../ensureApiEnabled";
import { Options } from "../options";
+import { Constants } from "../emulator/constants";
const TIMEOUT_MILLIS = 30000;
const MAXIMUM_PROMPT_LIST = 100;
@@ -46,6 +47,9 @@ export async function promptProjectCreation(
} else if (projectId.length > 30) {
return "Project ID cannot be longer than 30 characters";
}
+ if (Constants.isDemoProject(projectId)) {
+ return "Project ID cannot starts with demo-";
+ }
try {
// Best effort. We should still allow project creation even if this fails.
@@ -172,7 +176,7 @@ export async function getOrPromptProject(
return selectProjectInteractively();
}
-async function selectProjectInteractively(
+export async function selectProjectInteractively(
pageSize: number = MAXIMUM_PROMPT_LIST,
): Promise {
const { projects, nextPageToken } = await getFirebaseProjectPage(pageSize);
@@ -182,15 +186,20 @@ async function selectProjectInteractively(
if (nextPageToken) {
// Prompt user for project ID if we can't list all projects in 1 page
logger.debug(`Found more than ${projects.length} projects, selecting via prompt`);
- return selectProjectByPrompting();
+ return await getFirebaseProject(await selectProjectByPrompting());
}
return selectProjectFromList(projects);
}
-async function selectProjectByPrompting(): Promise {
+async function selectProjectByPrompting(): Promise {
const projectId = await prompt.input("Please input the project ID you would like to use:");
-
- return await getFirebaseProject(projectId);
+ if (!projectId) {
+ throw new FirebaseError("Project ID cannot be empty");
+ }
+ if (Constants.isDemoProject(projectId)) {
+ throw new FirebaseError("Project ID cannot starts with demo-");
+ }
+ return projectId;
}
/**
@@ -251,10 +260,9 @@ export async function promptAvailableProjectId(): Promise {
}
if (nextPageToken) {
- // Prompt for project ID if we can't list all projects in 1 page
- return await prompt.input(
- "Please input the ID of the Google Cloud Project you would like to add Firebase:",
- );
+ // Prompt user for project ID if we can't list all projects in 1 page
+ logger.debug(`Found more than ${projects.length} projects, selecting via prompt`);
+ return await selectProjectByPrompting();
} else {
const choices = projects
.filter((p: CloudProjectInfo) => !!p)