firebase-tools 14.10.0 → 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.
Files changed (39) hide show
  1. package/lib/api.js +3 -1
  2. package/lib/apphosting/backend.js +5 -2
  3. package/lib/apptesting/invokeTests.js +40 -0
  4. package/lib/apptesting/parseTestFiles.js +75 -0
  5. package/lib/apptesting/types.js +18 -0
  6. package/lib/commands/apptesting-execute.js +102 -0
  7. package/lib/commands/index.js +4 -0
  8. package/lib/commands/init.js +7 -0
  9. package/lib/deploy/apphosting/prepare.js +1 -9
  10. package/lib/deploy/functions/runtimes/discovery/index.js +53 -4
  11. package/lib/deploy/functions/runtimes/node/index.js +74 -44
  12. package/lib/emulator/dataconnect/pgliteServer.js +4 -4
  13. package/lib/emulator/downloadableEmulatorInfo.json +18 -18
  14. package/lib/emulator/env.js +2 -1
  15. package/lib/emulator/hub.js +1 -2
  16. package/lib/emulator/tasksEmulator.js +1 -1
  17. package/lib/emulator/ui.js +3 -1
  18. package/lib/experiments.js +4 -0
  19. package/lib/firestore/api.js +1 -11
  20. package/lib/init/features/aitools/claude.js +44 -0
  21. package/lib/init/features/aitools/cursor.js +62 -0
  22. package/lib/init/features/aitools/gemini.js +58 -0
  23. package/lib/init/features/aitools/index.js +28 -0
  24. package/lib/init/features/aitools/promptUpdater.js +109 -0
  25. package/lib/init/features/aitools/studio.js +17 -0
  26. package/lib/init/features/aitools/types.js +2 -0
  27. package/lib/init/features/aitools.js +83 -0
  28. package/lib/init/features/apptesting/index.js +29 -0
  29. package/lib/init/features/index.js +4 -1
  30. package/lib/init/index.js +5 -0
  31. package/lib/mcp/index.js +32 -1
  32. package/lib/utils.js +21 -1
  33. package/package.json +2 -1
  34. package/prompts/FIREBASE.md +122 -0
  35. package/prompts/FIREBASE_FUNCTIONS.md +221 -0
  36. package/schema/apptesting-yaml.json +64 -0
  37. package/templates/init/aitools/cursor-rules-header.txt +8 -0
  38. package/templates/init/aitools/gemini-extension.json +11 -0
  39. package/templates/init/apptesting/smoke_test.yaml +6 -0
@@ -54,28 +54,28 @@
54
54
  },
55
55
  "dataconnect": {
56
56
  "darwin": {
57
- "version": "2.9.0",
58
- "expectedSize": 27415296,
59
- "expectedChecksum": "5814d22b06b1321409f40693dfe47288",
60
- "expectedChecksumSHA256": "8df1cfac6ef747909b3c18b57ba52b70a11d53b9d0a22b197011d6550ceca133",
61
- "remoteUrl": "https://storage.googleapis.com/firemat-preview-drop/emulator/dataconnect-emulator-macos-v2.9.0",
62
- "downloadPathRelativeToCacheDir": "dataconnect-emulator-2.9.0"
57
+ "version": "2.10.0",
58
+ "expectedSize": 29332320,
59
+ "expectedChecksum": "8e1f4303580e91017d449a091f987caa",
60
+ "expectedChecksumSHA256": "000356e1a93a8eb55ff6ba5e268b5dd4c0638636708e95874a74b859aa977cb3",
61
+ "remoteUrl": "https://storage.googleapis.com/firemat-preview-drop/emulator/dataconnect-emulator-macos-v2.10.0",
62
+ "downloadPathRelativeToCacheDir": "dataconnect-emulator-2.10.0"
63
63
  },
64
64
  "win32": {
65
- "version": "2.9.0",
66
- "expectedSize": 27875328,
67
- "expectedChecksum": "21381052c2bf767ee84fb85975cfd4d5",
68
- "expectedChecksumSHA256": "6bebaa9575b460c4fa170b191d94722923a3c869bc1eca24c7e444428e6b7a70",
69
- "remoteUrl": "https://storage.googleapis.com/firemat-preview-drop/emulator/dataconnect-emulator-windows-v2.9.0",
70
- "downloadPathRelativeToCacheDir": "dataconnect-emulator-2.9.0.exe"
65
+ "version": "2.10.0",
66
+ "expectedSize": 29824000,
67
+ "expectedChecksum": "ea880276edee4908c4a6def6b1c7e2c1",
68
+ "expectedChecksumSHA256": "99ab61eba3de0cf14ef4ed5597f54e81ea8383b6782beba4b9b3f884acae4629",
69
+ "remoteUrl": "https://storage.googleapis.com/firemat-preview-drop/emulator/dataconnect-emulator-windows-v2.10.0",
70
+ "downloadPathRelativeToCacheDir": "dataconnect-emulator-2.10.0.exe"
71
71
  },
72
72
  "linux": {
73
- "version": "2.9.0",
74
- "expectedSize": 27332760,
75
- "expectedChecksum": "0480a92f5af8e8441e680dc4e7995263",
76
- "expectedChecksumSHA256": "4f60baf4649c670227314302056f50b1c40ccc8d673b98d16935977508342a67",
77
- "remoteUrl": "https://storage.googleapis.com/firemat-preview-drop/emulator/dataconnect-emulator-linux-v2.9.0",
78
- "downloadPathRelativeToCacheDir": "dataconnect-emulator-2.9.0"
73
+ "version": "2.10.0",
74
+ "expectedSize": 29257912,
75
+ "expectedChecksum": "fa64e70507e70b8d8063ca0993a3b07a",
76
+ "expectedChecksumSHA256": "76880201ff7204c32cbbaa2c0cbf6a30528486eb1f2948064dcd218bceaab65e",
77
+ "remoteUrl": "https://storage.googleapis.com/firemat-preview-drop/emulator/dataconnect-emulator-linux-v2.10.0",
78
+ "downloadPathRelativeToCacheDir": "dataconnect-emulator-2.10.0"
79
79
  }
80
80
  }
81
81
  }
@@ -69,7 +69,8 @@ function maybeUsePortForwarding(i) {
69
69
  }
70
70
  const url = `${info.port}-${portForwardingHost}`;
71
71
  info.host = url;
72
- info.listen = (_a = info.listen) === null || _a === void 0 ? void 0 : _a.map((l) => {
72
+ info.listen = (_a = info.listen) === null || _a === void 0 ? void 0 : _a.map((listen) => {
73
+ const l = Object.assign({}, listen);
73
74
  l.address = url;
74
75
  l.port = 443;
75
76
  return l;
@@ -11,7 +11,6 @@ const hubExport_1 = require("./hubExport");
11
11
  const registry_1 = require("./registry");
12
12
  const ExpressBasedEmulator_1 = require("./ExpressBasedEmulator");
13
13
  const vsCodeUtils_1 = require("../vsCodeUtils");
14
- const env_1 = require("./env");
15
14
  const pkg = require("../../package.json");
16
15
  class EmulatorHub extends ExpressBasedEmulator_1.ExpressBasedEmulator {
17
16
  static readLocatorFile(projectId) {
@@ -45,7 +44,7 @@ class EmulatorHub extends ExpressBasedEmulator_1.ExpressBasedEmulator {
45
44
  getRunningEmulatorsMapping() {
46
45
  const emulators = {};
47
46
  for (const info of registry_1.EmulatorRegistry.listRunningWithInfo()) {
48
- emulators[info.name] = (0, env_1.maybeUsePortForwarding)(Object.assign({ listen: this.args.listenForEmulator[info.name] }, info));
47
+ emulators[info.name] = Object.assign({ listen: this.args.listenForEmulator[info.name] }, info);
49
48
  }
50
49
  return emulators;
51
50
  }
@@ -166,7 +166,7 @@ class TasksEmulator {
166
166
  }
167
167
  req.body.task.name =
168
168
  (_a = req.body.task.name) !== null && _a !== void 0 ? _a : `/projects/${projectId}/locations/${locationId}/queues/${queueName}/tasks/${Math.floor(Math.random() * Number.MAX_SAFE_INTEGER)}`;
169
- req.body.task.httpRequest.body = JSON.parse(atob(req.body.task.httpRequest.body));
169
+ req.body.task.httpRequest.body = JSON.parse(Buffer.from(req.body.task.httpRequest.body, "base64").toString("utf-8"));
170
170
  const task = req.body.task;
171
171
  try {
172
172
  this.controller.enqueue(queueKey, task);
@@ -12,6 +12,8 @@ const constants_1 = require("./constants");
12
12
  const track_1 = require("../track");
13
13
  const ExpressBasedEmulator_1 = require("./ExpressBasedEmulator");
14
14
  const experiments_1 = require("../experiments");
15
+ const functional_1 = require("../functional");
16
+ const env_1 = require("./env");
15
17
  class EmulatorUI extends ExpressBasedEmulator_1.ExpressBasedEmulator {
16
18
  constructor(args) {
17
19
  super({
@@ -35,7 +37,7 @@ class EmulatorUI extends ExpressBasedEmulator_1.ExpressBasedEmulator {
35
37
  const downloadDetails = downloadableEmulators.getDownloadDetails(types_1.Emulators.UI);
36
38
  const webDir = path.join(downloadDetails.unzipDir, "client");
37
39
  app.get("/api/config", this.jsonHandler(() => {
38
- const emulatorInfos = hub.getRunningEmulatorsMapping();
40
+ const emulatorInfos = (0, functional_1.mapObject)(hub.getRunningEmulatorsMapping(), env_1.maybeUsePortForwarding);
39
41
  const json = Object.assign({ projectId, experiments: enabledExperiments !== null && enabledExperiments !== void 0 ? enabledExperiments : [], analytics: emulatorGaSession }, emulatorInfos);
40
42
  return Promise.resolve(json);
41
43
  }));
@@ -105,6 +105,10 @@ exports.ALL_EXPERIMENTS = experiments({
105
105
  default: true,
106
106
  public: false,
107
107
  },
108
+ apptesting: {
109
+ shortDescription: "Adds experimental App Testing feature",
110
+ public: true,
111
+ },
108
112
  });
109
113
  function isValidExperiment(name) {
110
114
  return Object.keys(exports.ALL_EXPERIMENTS).includes(name);
@@ -126,17 +126,7 @@ class FirestoreApi {
126
126
  if (!indexes) {
127
127
  return [];
128
128
  }
129
- return indexes.map((index) => {
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,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -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) {