firebase-tools 14.10.1 → 14.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/api.js +3 -1
- package/lib/apptesting/invokeTests.js +40 -0
- package/lib/apptesting/parseTestFiles.js +75 -0
- package/lib/apptesting/types.js +18 -0
- package/lib/commands/apptesting-execute.js +102 -0
- package/lib/commands/index.js +4 -0
- package/lib/commands/init.js +7 -0
- package/lib/deploy/functions/runtimes/discovery/index.js +53 -4
- package/lib/deploy/functions/runtimes/node/index.js +74 -44
- package/lib/emulator/downloadableEmulatorInfo.json +18 -18
- package/lib/emulator/env.js +2 -1
- package/lib/emulator/hub.js +1 -2
- package/lib/emulator/tasksEmulator.js +1 -1
- package/lib/emulator/ui.js +3 -1
- package/lib/experiments.js +4 -0
- package/lib/firestore/api.js +1 -11
- package/lib/init/features/aitools/claude.js +44 -0
- package/lib/init/features/aitools/cursor.js +62 -0
- package/lib/init/features/aitools/gemini.js +58 -0
- package/lib/init/features/aitools/index.js +28 -0
- package/lib/init/features/aitools/promptUpdater.js +109 -0
- package/lib/init/features/aitools/studio.js +17 -0
- package/lib/init/features/aitools/types.js +2 -0
- package/lib/init/features/aitools.js +83 -0
- package/lib/init/features/apptesting/index.js +29 -0
- package/lib/init/features/index.js +4 -1
- package/lib/init/index.js +5 -0
- package/lib/mcp/index.js +32 -1
- package/lib/utils.js +21 -1
- package/package.json +2 -1
- package/prompts/FIREBASE.md +122 -0
- package/prompts/FIREBASE_FUNCTIONS.md +221 -0
- package/schema/apptesting-yaml.json +64 -0
- package/templates/init/aitools/cursor-rules-header.txt +8 -0
- package/templates/init/aitools/gemini-extension.json +11 -0
- package/templates/init/apptesting/smoke_test.yaml +6 -0
package/lib/firestore/api.js
CHANGED
|
@@ -126,17 +126,7 @@ class FirestoreApi {
|
|
|
126
126
|
if (!indexes) {
|
|
127
127
|
return [];
|
|
128
128
|
}
|
|
129
|
-
return indexes
|
|
130
|
-
const fields = index.fields.filter((field) => {
|
|
131
|
-
return field.fieldPath !== "__name__";
|
|
132
|
-
});
|
|
133
|
-
return {
|
|
134
|
-
name: index.name,
|
|
135
|
-
state: index.state,
|
|
136
|
-
queryScope: index.queryScope,
|
|
137
|
-
fields,
|
|
138
|
-
};
|
|
139
|
-
});
|
|
129
|
+
return indexes;
|
|
140
130
|
}
|
|
141
131
|
async listFieldOverrides(project, databaseId = "(default)") {
|
|
142
132
|
const parent = `projects/${project}/databases/${databaseId}/collectionGroups/-`;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.claude = void 0;
|
|
4
|
+
const promptUpdater_1 = require("./promptUpdater");
|
|
5
|
+
const CLAUDE_SETTINGS_PATH = ".claude/settings.local.json";
|
|
6
|
+
const CLAUDE_PROMPT_PATH = "CLAUDE.local.md";
|
|
7
|
+
exports.claude = {
|
|
8
|
+
name: "claude",
|
|
9
|
+
displayName: "Claude Code",
|
|
10
|
+
async configure(config, projectPath, enabledFeatures) {
|
|
11
|
+
var _a;
|
|
12
|
+
const files = [];
|
|
13
|
+
let existingConfig = {};
|
|
14
|
+
let settingsUpdated = false;
|
|
15
|
+
try {
|
|
16
|
+
const existingContent = config.readProjectFile(CLAUDE_SETTINGS_PATH);
|
|
17
|
+
if (existingContent) {
|
|
18
|
+
existingConfig = JSON.parse(existingContent);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
catch (e) {
|
|
22
|
+
}
|
|
23
|
+
if (!((_a = existingConfig.mcpServers) === null || _a === void 0 ? void 0 : _a.firebase)) {
|
|
24
|
+
if (!existingConfig.mcpServers) {
|
|
25
|
+
existingConfig.mcpServers = {};
|
|
26
|
+
}
|
|
27
|
+
existingConfig.mcpServers.firebase = {
|
|
28
|
+
command: "npx",
|
|
29
|
+
args: ["-y", "firebase-tools", "experimental:mcp", "--dir", projectPath],
|
|
30
|
+
};
|
|
31
|
+
config.writeProjectFile(CLAUDE_SETTINGS_PATH, JSON.stringify(existingConfig, null, 2));
|
|
32
|
+
settingsUpdated = true;
|
|
33
|
+
}
|
|
34
|
+
files.push({ path: CLAUDE_SETTINGS_PATH, updated: settingsUpdated });
|
|
35
|
+
const { updated } = await (0, promptUpdater_1.updateFirebaseSection)(config, CLAUDE_PROMPT_PATH, enabledFeatures, {
|
|
36
|
+
interactive: true,
|
|
37
|
+
});
|
|
38
|
+
files.push({
|
|
39
|
+
path: CLAUDE_PROMPT_PATH,
|
|
40
|
+
updated,
|
|
41
|
+
});
|
|
42
|
+
return { files };
|
|
43
|
+
},
|
|
44
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.cursor = void 0;
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const templates_1 = require("../../../templates");
|
|
6
|
+
const promptUpdater_1 = require("./promptUpdater");
|
|
7
|
+
const CURSOR_MCP_PATH = ".cursor/mcp.json";
|
|
8
|
+
const CURSOR_RULES_DIR = ".cursor/rules";
|
|
9
|
+
exports.cursor = {
|
|
10
|
+
name: "cursor",
|
|
11
|
+
displayName: "Cursor",
|
|
12
|
+
async configure(config, projectPath, enabledFeatures) {
|
|
13
|
+
var _a;
|
|
14
|
+
const files = [];
|
|
15
|
+
let mcpUpdated = false;
|
|
16
|
+
let existingMcpConfig = {};
|
|
17
|
+
try {
|
|
18
|
+
const existingMcp = config.readProjectFile(CURSOR_MCP_PATH);
|
|
19
|
+
if (existingMcp) {
|
|
20
|
+
existingMcpConfig = JSON.parse(existingMcp);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
catch (e) {
|
|
24
|
+
}
|
|
25
|
+
if (!((_a = existingMcpConfig.mcpServers) === null || _a === void 0 ? void 0 : _a.firebase)) {
|
|
26
|
+
if (!existingMcpConfig.mcpServers) {
|
|
27
|
+
existingMcpConfig.mcpServers = {};
|
|
28
|
+
}
|
|
29
|
+
existingMcpConfig.mcpServers.firebase = {
|
|
30
|
+
command: "npx",
|
|
31
|
+
args: ["-y", "firebase-tools", "experimental:mcp", "--dir", projectPath],
|
|
32
|
+
};
|
|
33
|
+
config.writeProjectFile(CURSOR_MCP_PATH, JSON.stringify(existingMcpConfig, null, 2));
|
|
34
|
+
mcpUpdated = true;
|
|
35
|
+
}
|
|
36
|
+
files.push({ path: CURSOR_MCP_PATH, updated: mcpUpdated });
|
|
37
|
+
const header = (0, templates_1.readTemplateSync)("init/aitools/cursor-rules-header.txt");
|
|
38
|
+
const baseContent = (0, promptUpdater_1.generateFeaturePromptSection)("base");
|
|
39
|
+
const basePromptPath = path.join(CURSOR_RULES_DIR, "FIREBASE_BASE.md");
|
|
40
|
+
const baseResult = await (0, promptUpdater_1.replaceFirebaseFile)(config, basePromptPath, baseContent);
|
|
41
|
+
files.push({ path: basePromptPath, updated: baseResult.updated });
|
|
42
|
+
if (enabledFeatures.includes("functions")) {
|
|
43
|
+
const functionsContent = (0, promptUpdater_1.generateFeaturePromptSection)("functions");
|
|
44
|
+
const functionsPromptPath = path.join(CURSOR_RULES_DIR, "FIREBASE_FUNCTIONS.md");
|
|
45
|
+
const functionsResult = await (0, promptUpdater_1.replaceFirebaseFile)(config, functionsPromptPath, functionsContent);
|
|
46
|
+
files.push({ path: functionsPromptPath, updated: functionsResult.updated });
|
|
47
|
+
}
|
|
48
|
+
const imports = ["@FIREBASE_BASE.md"];
|
|
49
|
+
if (enabledFeatures.includes("functions")) {
|
|
50
|
+
imports.push("@FIREBASE_FUNCTIONS.md");
|
|
51
|
+
}
|
|
52
|
+
const importContent = `# Firebase Context\n\n${imports.join("\n")}\n`;
|
|
53
|
+
const { content: mainContent } = (0, promptUpdater_1.generatePromptSection)(enabledFeatures, {
|
|
54
|
+
customContent: importContent,
|
|
55
|
+
});
|
|
56
|
+
const fullContent = header + "\n" + mainContent;
|
|
57
|
+
const firebaseMDCPath = path.join(CURSOR_RULES_DIR, "FIREBASE.mdc");
|
|
58
|
+
const mainResult = await (0, promptUpdater_1.replaceFirebaseFile)(config, firebaseMDCPath, fullContent);
|
|
59
|
+
files.push({ path: firebaseMDCPath, updated: mainResult.updated });
|
|
60
|
+
return { files };
|
|
61
|
+
},
|
|
62
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.gemini = void 0;
|
|
4
|
+
const templates_1 = require("../../../templates");
|
|
5
|
+
const promptUpdater_1 = require("./promptUpdater");
|
|
6
|
+
const utils_1 = require("../../../utils");
|
|
7
|
+
const GEMINI_DIR = ".gemini/extensions/firebase";
|
|
8
|
+
const CONTEXTS_DIR = `${GEMINI_DIR}/contexts`;
|
|
9
|
+
exports.gemini = {
|
|
10
|
+
name: "gemini",
|
|
11
|
+
displayName: "Gemini CLI",
|
|
12
|
+
async configure(config, projectPath, enabledFeatures) {
|
|
13
|
+
const files = [];
|
|
14
|
+
const extensionPath = `${GEMINI_DIR}/gemini-extension.json`;
|
|
15
|
+
const extensionTemplate = (0, templates_1.readTemplateSync)("init/aitools/gemini-extension.json");
|
|
16
|
+
const newConfigRaw = extensionTemplate.replace("{{PROJECT_PATH}}", projectPath);
|
|
17
|
+
let extensionUpdated = false;
|
|
18
|
+
try {
|
|
19
|
+
const existingRaw = config.readProjectFile(extensionPath);
|
|
20
|
+
const existingConfig = JSON.parse(existingRaw);
|
|
21
|
+
const newConfig = JSON.parse(newConfigRaw);
|
|
22
|
+
if (!(0, utils_1.deepEqual)(existingConfig, newConfig)) {
|
|
23
|
+
config.writeProjectFile(extensionPath, newConfigRaw);
|
|
24
|
+
extensionUpdated = true;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
catch (_a) {
|
|
28
|
+
config.writeProjectFile(extensionPath, newConfigRaw);
|
|
29
|
+
extensionUpdated = true;
|
|
30
|
+
}
|
|
31
|
+
files.push({ path: extensionPath, updated: extensionUpdated });
|
|
32
|
+
const baseContent = (0, promptUpdater_1.generateFeaturePromptSection)("base");
|
|
33
|
+
const basePath = `${CONTEXTS_DIR}/FIREBASE-BASE.md`;
|
|
34
|
+
const baseResult = await (0, promptUpdater_1.replaceFirebaseFile)(config, basePath, baseContent);
|
|
35
|
+
files.push({ path: basePath, updated: baseResult.updated });
|
|
36
|
+
const imports = [
|
|
37
|
+
"# Firebase Context",
|
|
38
|
+
"",
|
|
39
|
+
"<!-- Import base Firebase context -->",
|
|
40
|
+
`@./contexts/FIREBASE-BASE.md`,
|
|
41
|
+
];
|
|
42
|
+
if (enabledFeatures.includes("functions")) {
|
|
43
|
+
const functionsContent = (0, promptUpdater_1.generateFeaturePromptSection)("functions");
|
|
44
|
+
const functionsPath = `${CONTEXTS_DIR}/FIREBASE-FUNCTIONS.md`;
|
|
45
|
+
const functionsResult = await (0, promptUpdater_1.replaceFirebaseFile)(config, functionsPath, functionsContent);
|
|
46
|
+
files.push({ path: functionsPath, updated: functionsResult.updated });
|
|
47
|
+
imports.push("", "<!-- Import Firebase Functions context -->", `@./contexts/FIREBASE-FUNCTIONS.md`);
|
|
48
|
+
}
|
|
49
|
+
const importContent = imports.join("\n");
|
|
50
|
+
const { content: mainContent } = (0, promptUpdater_1.generatePromptSection)(enabledFeatures, {
|
|
51
|
+
customContent: importContent,
|
|
52
|
+
});
|
|
53
|
+
const contextPath = `${GEMINI_DIR}/FIREBASE.md`;
|
|
54
|
+
const mainResult = await (0, promptUpdater_1.replaceFirebaseFile)(config, contextPath, mainContent);
|
|
55
|
+
files.push({ path: contextPath, updated: mainResult.updated });
|
|
56
|
+
return { files };
|
|
57
|
+
},
|
|
58
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.AI_TOOLS = void 0;
|
|
18
|
+
const cursor_1 = require("./cursor");
|
|
19
|
+
const gemini_1 = require("./gemini");
|
|
20
|
+
const studio_1 = require("./studio");
|
|
21
|
+
const claude_1 = require("./claude");
|
|
22
|
+
exports.AI_TOOLS = {
|
|
23
|
+
cursor: cursor_1.cursor,
|
|
24
|
+
gemini: gemini_1.gemini,
|
|
25
|
+
studio: studio_1.studio,
|
|
26
|
+
claude: claude_1.claude,
|
|
27
|
+
};
|
|
28
|
+
__exportStar(require("./types"), exports);
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.generateFeaturePromptSection = exports.getFeatureContent = exports.replaceFirebaseFile = exports.updateFirebaseSection = exports.generatePromptSection = void 0;
|
|
4
|
+
const crypto = require("crypto");
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
const prompt_1 = require("../../../prompt");
|
|
8
|
+
const utils = require("../../../utils");
|
|
9
|
+
const logger_1 = require("../../../logger");
|
|
10
|
+
const PROMPTS_DIR = path.join(__dirname, "../../../../prompts");
|
|
11
|
+
const FIREBASE_TAG_REGEX = /<firebase_prompts(?:\s+hash="([^"]+)")?>([\s\S]*?)<\/firebase_prompts>/;
|
|
12
|
+
const PROMPT_FILES = {
|
|
13
|
+
base: "FIREBASE.md",
|
|
14
|
+
functions: "FIREBASE_FUNCTIONS.md",
|
|
15
|
+
};
|
|
16
|
+
function calculateHash(content) {
|
|
17
|
+
return crypto.createHash("sha256").update(content.trim()).digest("hex").substring(0, 8);
|
|
18
|
+
}
|
|
19
|
+
function generatePromptSection(enabledFeatures, options) {
|
|
20
|
+
var _a;
|
|
21
|
+
let fullContent = getFeatureContent("base");
|
|
22
|
+
for (const feature of enabledFeatures) {
|
|
23
|
+
if (feature !== "base" && PROMPT_FILES[feature]) {
|
|
24
|
+
fullContent += "\n\n" + getFeatureContent(feature);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const hash = calculateHash(fullContent);
|
|
28
|
+
const innerContent = (_a = options === null || options === void 0 ? void 0 : options.customContent) !== null && _a !== void 0 ? _a : fullContent;
|
|
29
|
+
const wrapped = `<firebase_prompts hash="${hash}">
|
|
30
|
+
<!-- Firebase Tools Context - Auto-generated, do not edit -->
|
|
31
|
+
${innerContent}
|
|
32
|
+
</firebase_prompts>`;
|
|
33
|
+
return { content: wrapped, hash };
|
|
34
|
+
}
|
|
35
|
+
exports.generatePromptSection = generatePromptSection;
|
|
36
|
+
async function updateFirebaseSection(config, filePath, enabledFeatures, options) {
|
|
37
|
+
const { content: newSection, hash: newHash } = generatePromptSection(enabledFeatures);
|
|
38
|
+
let currentContent = "";
|
|
39
|
+
try {
|
|
40
|
+
currentContent = config.readProjectFile(filePath) || "";
|
|
41
|
+
}
|
|
42
|
+
catch (_a) {
|
|
43
|
+
}
|
|
44
|
+
const match = currentContent.match(FIREBASE_TAG_REGEX);
|
|
45
|
+
if (match && match[1] === newHash) {
|
|
46
|
+
return { updated: false };
|
|
47
|
+
}
|
|
48
|
+
if ((options === null || options === void 0 ? void 0 : options.interactive) && currentContent) {
|
|
49
|
+
const fileName = filePath.split("/").pop();
|
|
50
|
+
logger_1.logger.info();
|
|
51
|
+
utils.logBullet(`Update available for ${fileName}`);
|
|
52
|
+
const shouldUpdate = await (0, prompt_1.confirm)({
|
|
53
|
+
message: `Update Firebase section in ${fileName}?`,
|
|
54
|
+
default: true,
|
|
55
|
+
});
|
|
56
|
+
if (!shouldUpdate) {
|
|
57
|
+
return { updated: false };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
let finalContent;
|
|
61
|
+
if (!currentContent) {
|
|
62
|
+
finalContent = (options === null || options === void 0 ? void 0 : options.header) ? `${options.header}\n\n${newSection}` : newSection;
|
|
63
|
+
}
|
|
64
|
+
else if (match) {
|
|
65
|
+
finalContent =
|
|
66
|
+
currentContent.substring(0, match.index) +
|
|
67
|
+
newSection +
|
|
68
|
+
currentContent.substring(match.index + match[0].length);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
const separator = currentContent.endsWith("\n") ? "\n" : "\n\n";
|
|
72
|
+
finalContent = currentContent + separator + newSection;
|
|
73
|
+
}
|
|
74
|
+
config.writeProjectFile(filePath, finalContent);
|
|
75
|
+
return { updated: true };
|
|
76
|
+
}
|
|
77
|
+
exports.updateFirebaseSection = updateFirebaseSection;
|
|
78
|
+
async function replaceFirebaseFile(config, filePath, content) {
|
|
79
|
+
try {
|
|
80
|
+
const existing = config.readProjectFile(filePath);
|
|
81
|
+
if (existing === content) {
|
|
82
|
+
return { updated: false };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch (_a) {
|
|
86
|
+
}
|
|
87
|
+
config.writeProjectFile(filePath, content);
|
|
88
|
+
return { updated: true };
|
|
89
|
+
}
|
|
90
|
+
exports.replaceFirebaseFile = replaceFirebaseFile;
|
|
91
|
+
function getFeatureContent(feature) {
|
|
92
|
+
const filename = PROMPT_FILES[feature];
|
|
93
|
+
if (!filename)
|
|
94
|
+
return "";
|
|
95
|
+
const content = fs.readFileSync(path.join(PROMPTS_DIR, filename), "utf8");
|
|
96
|
+
return content;
|
|
97
|
+
}
|
|
98
|
+
exports.getFeatureContent = getFeatureContent;
|
|
99
|
+
function generateFeaturePromptSection(feature) {
|
|
100
|
+
const content = getFeatureContent(feature);
|
|
101
|
+
if (!content)
|
|
102
|
+
return "";
|
|
103
|
+
const hash = calculateHash(content);
|
|
104
|
+
return `<firebase_${feature}_prompts hash="${hash}">
|
|
105
|
+
<!-- Firebase ${feature.charAt(0).toUpperCase() + feature.slice(1)} Context - Auto-generated, do not edit -->
|
|
106
|
+
${content}
|
|
107
|
+
</firebase_${feature}_prompts>`;
|
|
108
|
+
}
|
|
109
|
+
exports.generateFeaturePromptSection = generateFeaturePromptSection;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.studio = void 0;
|
|
4
|
+
const promptUpdater_1 = require("./promptUpdater");
|
|
5
|
+
const RULES_PATH = ".idx/airules.md";
|
|
6
|
+
exports.studio = {
|
|
7
|
+
name: "studio",
|
|
8
|
+
displayName: "Firebase Studio",
|
|
9
|
+
async configure(config, projectPath, enabledFeatures) {
|
|
10
|
+
const files = [];
|
|
11
|
+
const { updated } = await (0, promptUpdater_1.updateFirebaseSection)(config, RULES_PATH, enabledFeatures, {
|
|
12
|
+
interactive: true,
|
|
13
|
+
});
|
|
14
|
+
files.push({ path: RULES_PATH, updated });
|
|
15
|
+
return { files };
|
|
16
|
+
},
|
|
17
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.doSetup = void 0;
|
|
4
|
+
const utils = require("../../utils");
|
|
5
|
+
const prompt_1 = require("../../prompt");
|
|
6
|
+
const index_1 = require("./aitools/index");
|
|
7
|
+
const logger_1 = require("../../logger");
|
|
8
|
+
const AGENT_CHOICES = Object.values(index_1.AI_TOOLS).map((tool) => ({
|
|
9
|
+
value: tool.name,
|
|
10
|
+
name: tool.displayName,
|
|
11
|
+
checked: false,
|
|
12
|
+
}));
|
|
13
|
+
async function doSetup(setup, config) {
|
|
14
|
+
logger_1.logger.info();
|
|
15
|
+
logger_1.logger.info("This command will configure AI coding assistants to work with your Firebase project by:");
|
|
16
|
+
utils.logBullet("• Setting up the Firebase MCP server for direct Firebase operations");
|
|
17
|
+
utils.logBullet("• Installing context files that help AI understand:");
|
|
18
|
+
utils.logBullet(" - Firebase project structure and firebase.json configuration");
|
|
19
|
+
utils.logBullet(" - Common Firebase CLI commands and debugging practices");
|
|
20
|
+
utils.logBullet(" - Product-specific guidance (Functions, Firestore, Hosting, etc.)");
|
|
21
|
+
logger_1.logger.info();
|
|
22
|
+
const selections = {};
|
|
23
|
+
selections.tools = await (0, prompt_1.checkbox)({
|
|
24
|
+
message: "Which tools would you like to configure?",
|
|
25
|
+
choices: AGENT_CHOICES,
|
|
26
|
+
validate: (choices) => {
|
|
27
|
+
if (choices.length === 0) {
|
|
28
|
+
return "Must select at least one tool.";
|
|
29
|
+
}
|
|
30
|
+
return true;
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
if (!selections.tools || selections.tools.length === 0) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
logger_1.logger.info();
|
|
37
|
+
logger_1.logger.info("Configuring selected tools...");
|
|
38
|
+
const projectPath = config.projectDir;
|
|
39
|
+
const enabledFeatures = getEnabledFeatures(setup.config);
|
|
40
|
+
let anyUpdates = false;
|
|
41
|
+
for (const toolName of selections.tools) {
|
|
42
|
+
const tool = index_1.AI_TOOLS[toolName];
|
|
43
|
+
if (!tool) {
|
|
44
|
+
utils.logWarning(`Unknown tool: ${toolName}`);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const result = await tool.configure(config, projectPath, enabledFeatures);
|
|
48
|
+
const updatedCount = result.files.filter((f) => f.updated).length;
|
|
49
|
+
const hasChanges = updatedCount > 0;
|
|
50
|
+
if (hasChanges) {
|
|
51
|
+
anyUpdates = true;
|
|
52
|
+
logger_1.logger.info();
|
|
53
|
+
utils.logSuccess(`${tool.displayName} configured - ${updatedCount} file${updatedCount > 1 ? "s" : ""} updated:`);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
logger_1.logger.info();
|
|
57
|
+
utils.logBullet(`${tool.displayName} - all files up to date`);
|
|
58
|
+
}
|
|
59
|
+
for (const file of result.files) {
|
|
60
|
+
const status = file.updated ? "(updated)" : "(unchanged)";
|
|
61
|
+
utils.logBullet(` ${file.path} ${status}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
logger_1.logger.info();
|
|
65
|
+
if (anyUpdates) {
|
|
66
|
+
utils.logSuccess("AI tools configuration complete!");
|
|
67
|
+
logger_1.logger.info();
|
|
68
|
+
logger_1.logger.info("Next steps:");
|
|
69
|
+
utils.logBullet("Restart your AI tools to load the new configuration");
|
|
70
|
+
utils.logBullet("Try asking your AI assistant about your Firebase project structure");
|
|
71
|
+
utils.logBullet("AI assistants now understand Firebase CLI commands and debugging");
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
utils.logSuccess("All AI tools are already up to date.");
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
exports.doSetup = doSetup;
|
|
78
|
+
function getEnabledFeatures(config) {
|
|
79
|
+
const features = [];
|
|
80
|
+
if (config.functions)
|
|
81
|
+
features.push("functions");
|
|
82
|
+
return features;
|
|
83
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.actuate = exports.askQuestions = void 0;
|
|
4
|
+
const path_1 = require("path");
|
|
5
|
+
const prompt_1 = require("../../../prompt");
|
|
6
|
+
const templates_1 = require("../../../templates");
|
|
7
|
+
const SMOKE_TEST_YAML_TEMPLATE = (0, templates_1.readTemplateSync)("init/apptesting/smoke_test.yaml");
|
|
8
|
+
async function askQuestions(setup) {
|
|
9
|
+
var _a, _b;
|
|
10
|
+
setup.featureInfo = Object.assign(Object.assign({}, setup.featureInfo), { apptesting: {
|
|
11
|
+
testDir: ((_b = (_a = setup.featureInfo) === null || _a === void 0 ? void 0 : _a.apptesting) === null || _b === void 0 ? void 0 : _b.testDir) ||
|
|
12
|
+
(await (0, prompt_1.input)({
|
|
13
|
+
message: "What do you want to use as your test directory?",
|
|
14
|
+
default: "tests",
|
|
15
|
+
})),
|
|
16
|
+
} });
|
|
17
|
+
}
|
|
18
|
+
exports.askQuestions = askQuestions;
|
|
19
|
+
async function actuate(setup, config) {
|
|
20
|
+
var _a;
|
|
21
|
+
const info = (_a = setup.featureInfo) === null || _a === void 0 ? void 0 : _a.apptesting;
|
|
22
|
+
if (!info) {
|
|
23
|
+
throw new Error("App Testing feature RequiredInfo is not provided");
|
|
24
|
+
}
|
|
25
|
+
const testDir = info.testDir;
|
|
26
|
+
config.set("apptesting.testDir", testDir);
|
|
27
|
+
await config.askWriteProjectFile((0, path_1.join)(testDir, "smoke_test.yaml"), SMOKE_TEST_YAML_TEMPLATE);
|
|
28
|
+
}
|
|
29
|
+
exports.actuate = actuate;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.genkit = exports.apphosting = exports.dataconnectSdk = exports.dataconnectPostSetup = exports.dataconnectActuate = exports.dataconnectAskQuestions = exports.hostingGithub = exports.remoteconfig = exports.project = exports.extensions = exports.emulators = exports.storageActuate = exports.storageAskQuestions = exports.hosting = exports.functions = exports.firestoreActuate = exports.firestoreAskQuestions = exports.databaseActuate = exports.databaseAskQuestions = exports.account = void 0;
|
|
3
|
+
exports.apptestingAcutate = exports.apptestingAskQuestions = exports.genkit = exports.apphosting = exports.dataconnectSdk = exports.dataconnectPostSetup = exports.dataconnectActuate = exports.dataconnectAskQuestions = exports.hostingGithub = exports.remoteconfig = exports.project = exports.extensions = exports.emulators = exports.storageActuate = exports.storageAskQuestions = exports.hosting = exports.functions = exports.firestoreActuate = exports.firestoreAskQuestions = exports.databaseActuate = exports.databaseAskQuestions = exports.account = void 0;
|
|
4
4
|
var account_1 = require("./account");
|
|
5
5
|
Object.defineProperty(exports, "account", { enumerable: true, get: function () { return account_1.doSetup; } });
|
|
6
6
|
var database_1 = require("./database");
|
|
@@ -36,3 +36,6 @@ var apphosting_1 = require("./apphosting");
|
|
|
36
36
|
Object.defineProperty(exports, "apphosting", { enumerable: true, get: function () { return apphosting_1.doSetup; } });
|
|
37
37
|
var genkit_1 = require("./genkit");
|
|
38
38
|
Object.defineProperty(exports, "genkit", { enumerable: true, get: function () { return genkit_1.doSetup; } });
|
|
39
|
+
var apptesting_1 = require("./apptesting");
|
|
40
|
+
Object.defineProperty(exports, "apptestingAskQuestions", { enumerable: true, get: function () { return apptesting_1.askQuestions; } });
|
|
41
|
+
Object.defineProperty(exports, "apptestingAcutate", { enumerable: true, get: function () { return apptesting_1.actuate; } });
|
package/lib/init/index.js
CHANGED
|
@@ -39,6 +39,11 @@ const featuresList = [
|
|
|
39
39
|
{ name: "hosting:github", doSetup: features.hostingGithub },
|
|
40
40
|
{ name: "genkit", doSetup: features.genkit },
|
|
41
41
|
{ name: "apphosting", displayName: "App Hosting", doSetup: features.apphosting },
|
|
42
|
+
{
|
|
43
|
+
name: "apptesting",
|
|
44
|
+
askQuestions: features.apptestingAskQuestions,
|
|
45
|
+
actuate: features.apptestingAcutate,
|
|
46
|
+
},
|
|
42
47
|
];
|
|
43
48
|
const featureMap = new Map(featuresList.map((feature) => [feature.name, feature]));
|
|
44
49
|
async function init(setup, config, options) {
|
package/lib/mcp/index.js
CHANGED
|
@@ -21,14 +21,28 @@ const ensureApiEnabled_js_1 = require("../ensureApiEnabled.js");
|
|
|
21
21
|
const api = require("../api.js");
|
|
22
22
|
const SERVER_VERSION = "0.1.0";
|
|
23
23
|
const cmd = new command_js_1.Command("experimental:mcp").before(requireAuth_js_1.requireAuth);
|
|
24
|
+
const orderedLogLevels = [
|
|
25
|
+
"debug",
|
|
26
|
+
"info",
|
|
27
|
+
"notice",
|
|
28
|
+
"warning",
|
|
29
|
+
"error",
|
|
30
|
+
"critical",
|
|
31
|
+
"alert",
|
|
32
|
+
"emergency",
|
|
33
|
+
];
|
|
24
34
|
class FirebaseMcpServer {
|
|
25
35
|
constructor(options) {
|
|
26
36
|
this._ready = false;
|
|
27
37
|
this._readyPromises = [];
|
|
38
|
+
this.logger = Object.fromEntries(orderedLogLevels.map((logLevel) => [
|
|
39
|
+
logLevel,
|
|
40
|
+
(message) => this.log(logLevel, message),
|
|
41
|
+
]));
|
|
28
42
|
this.activeFeatures = options.activeFeatures;
|
|
29
43
|
this.startupRoot = options.projectRoot || process.env.PROJECT_ROOT;
|
|
30
44
|
this.server = new index_js_1.Server({ name: "firebase", version: SERVER_VERSION });
|
|
31
|
-
this.server.registerCapabilities({ tools: { listChanged: true } });
|
|
45
|
+
this.server.registerCapabilities({ tools: { listChanged: true }, logging: {} });
|
|
32
46
|
this.server.setRequestHandler(types_js_1.ListToolsRequestSchema, this.mcpListTools.bind(this));
|
|
33
47
|
this.server.setRequestHandler(types_js_1.CallToolRequestSchema, this.mcpCallTool.bind(this));
|
|
34
48
|
this.server.oninitialized = async () => {
|
|
@@ -48,6 +62,10 @@ class FirebaseMcpServer {
|
|
|
48
62
|
(_b = this._readyPromises.pop()) === null || _b === void 0 ? void 0 : _b.resolve();
|
|
49
63
|
}
|
|
50
64
|
};
|
|
65
|
+
this.server.setRequestHandler(types_js_1.SetLevelRequestSchema, async ({ params }) => {
|
|
66
|
+
this.currentLogLevel = params.level;
|
|
67
|
+
return {};
|
|
68
|
+
});
|
|
51
69
|
this.detectProjectRoot();
|
|
52
70
|
this.detectActiveFeatures();
|
|
53
71
|
}
|
|
@@ -229,5 +247,18 @@ class FirebaseMcpServer {
|
|
|
229
247
|
const transport = new stdio_js_1.StdioServerTransport();
|
|
230
248
|
await this.server.connect(transport);
|
|
231
249
|
}
|
|
250
|
+
async log(level, message) {
|
|
251
|
+
let data = message;
|
|
252
|
+
if (typeof message === "string") {
|
|
253
|
+
data = { message };
|
|
254
|
+
}
|
|
255
|
+
if (!this.currentLogLevel) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (orderedLogLevels.indexOf(this.currentLogLevel) > orderedLogLevels.indexOf(level)) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
await this.server.sendLoggingMessage({ level, data });
|
|
262
|
+
}
|
|
232
263
|
}
|
|
233
264
|
exports.FirebaseMcpServer = FirebaseMcpServer;
|
package/lib/utils.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.getHostnameFromUrl = exports.openInBrowserPopup = exports.openInBrowser = exports.connectableHostname = exports.randomInt = exports.debounce = exports.last = exports.cloneDeep = exports.groupBy = exports.assertIsStringOrUndefined = exports.assertIsNumber = exports.assertIsString = exports.thirtyDaysFromNow = exports.isRunningInWSL = exports.isCloudEnvironment = exports.datetimeString = exports.createDestroyer = exports.sleep = exports.promiseWithSpinner = exports.tryParse = exports.promiseProps = exports.withTimeout = exports.promiseWhile = exports.promiseAllSettled = exports.getFunctionsEventProvider = exports.endpoint = exports.makeActiveProject = exports.streamToString = exports.stringToStream = exports.explainStdin = exports.allSettled = exports.reject = exports.logLabeledError = exports.logLabeledWarning = exports.logWarning = exports.logLabeledBullet = exports.logBullet = exports.logLabeledSuccess = exports.logSuccess = exports.addSubdomain = exports.addDatabaseNamespace = exports.getDatabaseViewDataUrl = exports.getDatabaseUrl = exports.envOverride = exports.setVSCodeEnvVars = exports.getInheritedOption = exports.consoleUrl = exports.vscodeEnvVars = exports.envOverrides = exports.IS_WINDOWS = void 0;
|
|
4
|
-
exports.promptForDirectory = exports.updateOrCreateGitignore = exports.readSecretValue = exports.generateId = exports.wrappedSafeLoad = exports.readFileFromDirectory = void 0;
|
|
4
|
+
exports.deepEqual = exports.promptForDirectory = exports.updateOrCreateGitignore = exports.readSecretValue = exports.generateId = exports.wrappedSafeLoad = exports.readFileFromDirectory = void 0;
|
|
5
5
|
const fs = require("fs-extra");
|
|
6
6
|
const tty = require("tty");
|
|
7
7
|
const path = require("node:path");
|
|
@@ -599,3 +599,23 @@ async function promptForDirectory(args) {
|
|
|
599
599
|
return dir;
|
|
600
600
|
}
|
|
601
601
|
exports.promptForDirectory = promptForDirectory;
|
|
602
|
+
function deepEqual(a, b) {
|
|
603
|
+
if (a === b) {
|
|
604
|
+
return true;
|
|
605
|
+
}
|
|
606
|
+
if (typeof a !== "object" || a === null || typeof b !== "object" || b === null) {
|
|
607
|
+
return false;
|
|
608
|
+
}
|
|
609
|
+
const keysA = Object.keys(a);
|
|
610
|
+
const keysB = Object.keys(b);
|
|
611
|
+
if (keysA.length !== keysB.length) {
|
|
612
|
+
return false;
|
|
613
|
+
}
|
|
614
|
+
for (const key of keysA) {
|
|
615
|
+
if (!keysB.includes(key) || !deepEqual(a[key], b[key])) {
|
|
616
|
+
return false;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
return true;
|
|
620
|
+
}
|
|
621
|
+
exports.deepEqual = deepEqual;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "firebase-tools",
|
|
3
|
-
"version": "14.
|
|
3
|
+
"version": "14.11.0",
|
|
4
4
|
"description": "Command-Line Interface for Firebase",
|
|
5
5
|
"main": "./lib/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"lib",
|
|
11
|
+
"prompts",
|
|
11
12
|
"schema",
|
|
12
13
|
"standalone",
|
|
13
14
|
"templates"
|