Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
2b87a86
Add Firebase App provisioning API integration
TrCaM Sep 19, 2025
74f3687
Firebase MCP init tool schema change to support provisioning
TrCaM Sep 22, 2025
6c2b93a
Add methods to detect existing local apps from directories
TrCaM Sep 22, 2025
31e6327
Add methods to ensure local file locations for app provisioning
TrCaM Sep 22, 2025
b68727f
Wire up provisioning logic with fake API service - Verify logic first…
TrCaM Sep 22, 2025
1c2cc10
Archive mock provisioning service for future reference
TrCaM Sep 24, 2025
d411761
Complete first working version of revamped firebase init MCP tool
TrCaM Sep 24, 2025
a1cd048
Merge master into feature branch
TrCaM Sep 24, 2025
647eb87
Fix lint/formating - Add unit test for new firebase init tool
TrCaM Sep 24, 2025
3309d71
Fix lint/formating - Add unit test for new firebase init tool
TrCaM Sep 24, 2025
0a0af59
Minor pr fixes
joehan Sep 24, 2025
193d196
Adding missing files
joehan Sep 24, 2025
5f3a6b9
Merge branch 'master' into jh-provisioning
joehan Sep 24, 2025
e3afdee
Merge branch 'master' into jh-provisioning
joehan Sep 24, 2025
9814ef9
Merge branch 'master' into jh-provisioning
joehan Sep 24, 2025
6bedcaf
Reduce provisioning integration scope for only AI Logic
TrCaM Sep 25, 2025
265d339
Complete orchestration API provisioning to AI Logic feature (Both CLI…
TrCaM Sep 25, 2025
8b0ff79
Merge remote-tracking branch 'origin/master' into caot/mcp/ailogic
TrCaM Sep 25, 2025
d032ff7
Fix lint and unit tests
TrCaM Sep 25, 2025
82d5443
Address Gemini code assist comments
TrCaM Sep 25, 2025
c3b33c6
Simplify ailogic init feature to only use existing project and app
TrCaM Sep 27, 2025
a7de790
Merge remote-tracking branch 'origin/master' into caot-mcp-ailogic
TrCaM Sep 27, 2025
ade68ce
Fix linting
TrCaM Sep 27, 2025
f1906de
Addresses comments
TrCaM Sep 30, 2025
671cec2
Merge remote-tracking branch 'origin/master' into caot-mcp-ailogic
TrCaM Sep 30, 2025
9dc2299
Check projectId is non-empty for ailogic feature (MCP tool)
TrCaM Sep 30, 2025
c8c02ae
Merge branch 'master' into caot-mcp-ailogic
joehan Oct 1, 2025
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
6 changes: 6 additions & 0 deletions src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,12 @@
});
}

choices.push({
value: "ailogic",
name: "AI Logic: Set up Firebase AI Logic with app provisioning",
checked: false,
});

choices.push({
value: "aitools",
name: "AI Tools: Configure AI coding assistants to work with your Firebase project",
Expand Down Expand Up @@ -194,7 +200,7 @@

const setup: Setup = {
config: config.src,
rcfile: config.readProjectFile(".firebaserc", {

Check warning on line 203 in src/commands/init.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe assignment of an `any` value
json: true,
fallback: {},
}),
Expand Down
242 changes: 242 additions & 0 deletions src/init/features/ailogic/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
import { expect } from "chai";
import * as sinon from "sinon";
import * as fs from "fs-extra";
import * as init from "./index";
import * as utils from "./utils";
import { Setup } from "../..";
import { Config } from "../../../config";
import { Platform } from "../../../dataconnect/types";

Check failure on line 8 in src/init/features/ailogic/index.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

'Platform' is defined but never used

Check failure on line 8 in src/init/features/ailogic/index.spec.ts

View workflow job for this annotation

GitHub Actions / unit (20)

'Platform' is defined but never used

Check failure on line 8 in src/init/features/ailogic/index.spec.ts

View workflow job for this annotation

GitHub Actions / unit (22)

'Platform' is defined but never used

describe("init ailogic", () => {
let sandbox: sinon.SinonSandbox;

beforeEach(() => {
sandbox = sinon.createSandbox();
});

afterEach(() => {
sandbox.restore();
});

describe("askQuestions", () => {
it("should complete without throwing", async () => {
// Skip detailed testing of askQuestions for now - it involves complex prompt mocking
const mockSetup = { featureInfo: {} } as Setup;
const mockConfig = {} as Config;

// This test just ensures the function signature is correct
// Real functionality testing would require mocking dynamic imports
expect(() => init.askQuestions(mockSetup, mockConfig)).to.not.throw();
});
});

describe("actuate", () => {
let setup: Setup;
let config: Config;
let detectAppPlatformStub: sinon.SinonStub;
let buildProvisionOptionsStub: sinon.SinonStub;
let provisionAiLogicAppStub: sinon.SinonStub;
let writeAppConfigFileStub: sinon.SinonStub;
let extractProjectIdStub: sinon.SinonStub;
let getConfigFilePathStub: sinon.SinonStub;
let existsSyncStub: sinon.SinonStub;

beforeEach(() => {
setup = {
config: {} as any,

Check warning on line 46 in src/init/features/ailogic/index.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type

Check warning on line 46 in src/init/features/ailogic/index.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe assignment of an `any` value
rcfile: { projects: {}, targets: {}, etags: {} },
featureInfo: {
ailogic: {
appNamespace: "com.example.test",
appPlatform: "android",
overwriteConfig: false,
},
},
projectId: "test-project",
instructions: [],
} as Setup;

config = {
projectDir: "/test/project",
} as Config;

// Stub all utility functions
detectAppPlatformStub = sandbox.stub(utils, "detectAppPlatform");
buildProvisionOptionsStub = sandbox.stub(utils, "buildProvisionOptions");
provisionAiLogicAppStub = sandbox.stub(utils, "provisionAiLogicApp");
writeAppConfigFileStub = sandbox.stub(utils, "writeAppConfigFile");
extractProjectIdStub = sandbox.stub(utils, "extractProjectIdFromAppResource");
getConfigFilePathStub = sandbox.stub(utils, "getConfigFilePath");
existsSyncStub = sandbox.stub(fs, "existsSync");
});

it("should return early if no ailogic feature info", async () => {
setup.featureInfo = {};

await init.actuate(setup, config);

// No stubs should be called
sinon.assert.notCalled(detectAppPlatformStub);
sinon.assert.notCalled(provisionAiLogicAppStub);
});

it("should use provided app platform", async () => {
const configFilePath = "/test/project/google-services.json";
getConfigFilePathStub.returns(configFilePath);
existsSyncStub.returns(false);
buildProvisionOptionsStub.returns({ mock: "options" });
provisionAiLogicAppStub.returns({
appResource: "projects/test-project/apps/test-app",
configData: "base64config",
});
extractProjectIdStub.returns("test-project");

await init.actuate(setup, config);

// Should not call detectAppPlatform since platform is provided
sinon.assert.notCalled(detectAppPlatformStub);
sinon.assert.calledWith(getConfigFilePathStub, "/test/project", "android");
sinon.assert.calledWith(buildProvisionOptionsStub, "test-project", "android", "com.example.test");

Check failure on line 99 in src/init/features/ailogic/index.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Replace `buildProvisionOptionsStub,·"test-project",·"android",·"com.example.test"` with `⏎········buildProvisionOptionsStub,⏎········"test-project",⏎········"android",⏎········"com.example.test",⏎······`

Check failure on line 99 in src/init/features/ailogic/index.spec.ts

View workflow job for this annotation

GitHub Actions / unit (20)

Replace `buildProvisionOptionsStub,·"test-project",·"android",·"com.example.test"` with `⏎········buildProvisionOptionsStub,⏎········"test-project",⏎········"android",⏎········"com.example.test",⏎······`

Check failure on line 99 in src/init/features/ailogic/index.spec.ts

View workflow job for this annotation

GitHub Actions / unit (22)

Replace `buildProvisionOptionsStub,·"test-project",·"android",·"com.example.test"` with `⏎········buildProvisionOptionsStub,⏎········"test-project",⏎········"android",⏎········"com.example.test",⏎······`
});

it("should auto-detect platform when not provided", async () => {
setup.featureInfo!.ailogic!.appPlatform = undefined;

Check warning on line 103 in src/init/features/ailogic/index.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Forbidden non-null assertion

Check warning on line 103 in src/init/features/ailogic/index.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Forbidden non-null assertion
const configFilePath = "/test/project/firebase-config.json";

detectAppPlatformStub.returns("web");
getConfigFilePathStub.returns(configFilePath);
existsSyncStub.returns(false);
buildProvisionOptionsStub.returns({ mock: "options" });
provisionAiLogicAppStub.returns({
appResource: "projects/test-project/apps/test-app",
configData: "base64config",
});
extractProjectIdStub.returns("test-project");

await init.actuate(setup, config);

sinon.assert.calledWith(detectAppPlatformStub, "/test/project");
sinon.assert.calledWith(getConfigFilePathStub, "/test/project", "web");
sinon.assert.calledWith(buildProvisionOptionsStub, "test-project", "web", "com.example.test");
});

it("should throw error if config file exists and overwrite not enabled", async () => {
const configFilePath = "/test/project/google-services.json";
getConfigFilePathStub.returns(configFilePath);
existsSyncStub.returns(true);

await expect(init.actuate(setup, config)).to.be.rejectedWith(
"AI Logic setup failed: Config file /test/project/google-services.json already exists. Use overwrite_config: true to update it."

Check failure on line 129 in src/init/features/ailogic/index.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Insert `,`

Check failure on line 129 in src/init/features/ailogic/index.spec.ts

View workflow job for this annotation

GitHub Actions / unit (20)

Insert `,`

Check failure on line 129 in src/init/features/ailogic/index.spec.ts

View workflow job for this annotation

GitHub Actions / unit (22)

Insert `,`
);
});

it("should proceed if config file exists and overwrite is enabled", async () => {
setup.featureInfo!.ailogic!.overwriteConfig = true;

Check warning on line 134 in src/init/features/ailogic/index.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Forbidden non-null assertion

Check warning on line 134 in src/init/features/ailogic/index.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Forbidden non-null assertion
const configFilePath = "/test/project/google-services.json";

getConfigFilePathStub.returns(configFilePath);
existsSyncStub.returns(true);
buildProvisionOptionsStub.returns({ mock: "options" });
provisionAiLogicAppStub.returns({
appResource: "projects/test-project/apps/test-app",
configData: "base64config",
});
extractProjectIdStub.returns("test-project");

await init.actuate(setup, config);

sinon.assert.called(provisionAiLogicAppStub);
sinon.assert.calledWith(writeAppConfigFileStub, configFilePath, "base64config");
});

it("should provision app and write config file", async () => {
const configFilePath = "/test/project/google-services.json";
const mockResponse = {
appResource: "projects/new-project/apps/test-app",
configData: "base64configdata",
};

getConfigFilePathStub.returns(configFilePath);
existsSyncStub.returns(false);
buildProvisionOptionsStub.returns({ mock: "options" });
provisionAiLogicAppStub.returns(mockResponse);
extractProjectIdStub.returns("new-project");

await init.actuate(setup, config);

sinon.assert.calledWith(provisionAiLogicAppStub, { mock: "options" });
sinon.assert.calledWith(extractProjectIdStub, "projects/new-project/apps/test-app");
sinon.assert.calledWith(writeAppConfigFileStub, configFilePath, "base64configdata");
expect(setup.projectId).to.equal("new-project");
});

it("should update .firebaserc with new project", async () => {
const configFilePath = "/test/project/google-services.json";

getConfigFilePathStub.returns(configFilePath);
existsSyncStub.returns(false);
buildProvisionOptionsStub.returns({ mock: "options" });
provisionAiLogicAppStub.returns({
appResource: "projects/new-project/apps/test-app",
configData: "base64config",
});
extractProjectIdStub.returns("new-project");

await init.actuate(setup, config);

expect(setup.rcfile!.projects!.default).to.equal("new-project");

Check warning on line 187 in src/init/features/ailogic/index.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

This assertion is unnecessary since it does not change the type of the expression

Check warning on line 187 in src/init/features/ailogic/index.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Forbidden non-null assertion

Check warning on line 187 in src/init/features/ailogic/index.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

This assertion is unnecessary since it does not change the type of the expression
});

it("should add appropriate instructions", async () => {
const configFilePath = "/test/project/google-services.json";

getConfigFilePathStub.returns(configFilePath);
existsSyncStub.returns(false);
buildProvisionOptionsStub.returns({ mock: "options" });
provisionAiLogicAppStub.returns({
appResource: "projects/test-project/apps/test-app",
configData: "base64config",
});
extractProjectIdStub.returns("test-project");

await init.actuate(setup, config);

expect(setup.instructions).to.include("Firebase AI Logic has been enabled with a new android app.");

Check failure on line 204 in src/init/features/ailogic/index.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Replace `"Firebase·AI·Logic·has·been·enabled·with·a·new·android·app."` with `⏎········"Firebase·AI·Logic·has·been·enabled·with·a·new·android·app.",⏎······`

Check failure on line 204 in src/init/features/ailogic/index.spec.ts

View workflow job for this annotation

GitHub Actions / unit (20)

Replace `"Firebase·AI·Logic·has·been·enabled·with·a·new·android·app."` with `⏎········"Firebase·AI·Logic·has·been·enabled·with·a·new·android·app.",⏎······`

Check failure on line 204 in src/init/features/ailogic/index.spec.ts

View workflow job for this annotation

GitHub Actions / unit (22)

Replace `"Firebase·AI·Logic·has·been·enabled·with·a·new·android·app."` with `⏎········"Firebase·AI·Logic·has·been·enabled·with·a·new·android·app.",⏎······`
expect(setup.instructions).to.include(`Config file written to: ${configFilePath}`);
expect(setup.instructions).to.include("If you have multiple app directories, copy the config file to the appropriate app folder.");

Check failure on line 206 in src/init/features/ailogic/index.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Replace `"If·you·have·multiple·app·directories,·copy·the·config·file·to·the·appropriate·app·folder."` with `⏎········"If·you·have·multiple·app·directories,·copy·the·config·file·to·the·appropriate·app·folder.",⏎······`

Check failure on line 206 in src/init/features/ailogic/index.spec.ts

View workflow job for this annotation

GitHub Actions / unit (20)

Replace `"If·you·have·multiple·app·directories,·copy·the·config·file·to·the·appropriate·app·folder."` with `⏎········"If·you·have·multiple·app·directories,·copy·the·config·file·to·the·appropriate·app·folder.",⏎······`

Check failure on line 206 in src/init/features/ailogic/index.spec.ts

View workflow job for this annotation

GitHub Actions / unit (22)

Replace `"If·you·have·multiple·app·directories,·copy·the·config·file·to·the·appropriate·app·folder."` with `⏎········"If·you·have·multiple·app·directories,·copy·the·config·file·to·the·appropriate·app·folder.",⏎······`
expect(setup.instructions).to.include("Note: A new Firebase app was created. You can use existing Firebase apps with AI Logic (current API limitation).");

Check failure on line 207 in src/init/features/ailogic/index.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Replace `"Note:·A·new·Firebase·app·was·created.·You·can·use·existing·Firebase·apps·with·AI·Logic·(current·API·limitation)."` with `⏎········"Note:·A·new·Firebase·app·was·created.·You·can·use·existing·Firebase·apps·with·AI·Logic·(current·API·limitation).",⏎······`

Check failure on line 207 in src/init/features/ailogic/index.spec.ts

View workflow job for this annotation

GitHub Actions / unit (20)

Replace `"Note:·A·new·Firebase·app·was·created.·You·can·use·existing·Firebase·apps·with·AI·Logic·(current·API·limitation)."` with `⏎········"Note:·A·new·Firebase·app·was·created.·You·can·use·existing·Firebase·apps·with·AI·Logic·(current·API·limitation).",⏎······`

Check failure on line 207 in src/init/features/ailogic/index.spec.ts

View workflow job for this annotation

GitHub Actions / unit (22)

Replace `"Note:·A·new·Firebase·app·was·created.·You·can·use·existing·Firebase·apps·with·AI·Logic·(current·API·limitation)."` with `⏎········"Note:·A·new·Firebase·app·was·created.·You·can·use·existing·Firebase·apps·with·AI·Logic·(current·API·limitation).",⏎······`
});

it("should handle provisioning errors gracefully", async () => {
const configFilePath = "/test/project/google-services.json";

getConfigFilePathStub.returns(configFilePath);
existsSyncStub.returns(false);
buildProvisionOptionsStub.returns({ mock: "options" });
provisionAiLogicAppStub.throws(new Error("Provisioning API failed"));

await expect(init.actuate(setup, config)).to.be.rejectedWith(
"AI Logic setup failed: Provisioning API failed"

Check failure on line 219 in src/init/features/ailogic/index.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Insert `,`

Check failure on line 219 in src/init/features/ailogic/index.spec.ts

View workflow job for this annotation

GitHub Actions / unit (20)

Insert `,`

Check failure on line 219 in src/init/features/ailogic/index.spec.ts

View workflow job for this annotation

GitHub Actions / unit (22)

Insert `,`
);
});

it("should handle missing rcfile gracefully", async () => {
setup.rcfile = undefined as any;
const configFilePath = "/test/project/google-services.json";

getConfigFilePathStub.returns(configFilePath);
existsSyncStub.returns(false);
buildProvisionOptionsStub.returns({ mock: "options" });
provisionAiLogicAppStub.returns({
appResource: "projects/test-project/apps/test-app",
configData: "base64config",
});
extractProjectIdStub.returns("test-project");

// Should not throw an error
await init.actuate(setup, config);

sinon.assert.called(provisionAiLogicAppStub);
});
});
});

Check failure on line 242 in src/init/features/ailogic/index.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Insert `⏎`

Check failure on line 242 in src/init/features/ailogic/index.spec.ts

View workflow job for this annotation

GitHub Actions / unit (20)

Insert `⏎`

Check failure on line 242 in src/init/features/ailogic/index.spec.ts

View workflow job for this annotation

GitHub Actions / unit (22)

Insert `⏎`
Loading