firebase-tools 13.29.3 → 13.31.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/apphosting/secrets/dialogs.js +2 -2
- package/lib/commands/appdistribution-groups-list.js +1 -1
- package/lib/commands/appdistribution-testers-list.js +4 -4
- package/lib/commands/apphosting-backends-list.js +1 -1
- package/lib/commands/apphosting-secrets-describe.js +1 -1
- package/lib/commands/apps-android-sha-list.js +1 -1
- package/lib/commands/apps-create.js +1 -104
- package/lib/commands/apps-init.js +94 -0
- package/lib/commands/apps-list.js +1 -1
- package/lib/commands/apps-sdkconfig.js +3 -3
- package/lib/commands/database-instances-list.js +1 -1
- package/lib/commands/dataconnect-services-list.js +1 -1
- package/lib/commands/emulators-start.js +1 -1
- package/lib/commands/experiments-list.js +1 -1
- package/lib/commands/ext-dev-list.js +1 -1
- package/lib/commands/ext-dev-usage.js +1 -1
- package/lib/commands/functions-list.js +1 -1
- package/lib/commands/hosting-channel-list.js +1 -1
- package/lib/commands/hosting-sites-get.js +1 -1
- package/lib/commands/hosting-sites-list.js +1 -1
- package/lib/commands/index.js +1 -0
- package/lib/commands/projects-list.js +1 -1
- package/lib/commands/remoteconfig-get.js +1 -1
- package/lib/commands/remoteconfig-versions-list.js +1 -1
- package/lib/crashlytics/buildToolsJarHelper.js +1 -1
- package/lib/dataconnect/build.js +6 -0
- package/lib/dataconnect/client.js +1 -1
- package/lib/dataconnect/dataplaneClient.js +1 -1
- package/lib/dataconnect/fileUtils.js +25 -2
- package/lib/dataconnect/graphqlError.js +5 -3
- package/lib/emulator/commandUtils.js +1 -1
- package/lib/emulator/dataconnectEmulator.js +4 -1
- package/lib/emulator/dataconnectToolkitController.js +5 -0
- package/lib/emulator/downloadableEmulators.js +9 -9
- package/lib/emulator/extensionsEmulator.js +2 -2
- package/lib/experiments.js +6 -0
- package/lib/extensions/change-log.js +1 -25
- package/lib/extensions/listExtensions.js +1 -1
- package/lib/extensions/provisioningHelper.js +1 -1
- package/lib/firestore/pretty-print.js +1 -1
- package/lib/frameworks/angular/index.js +11 -6
- package/lib/frameworks/utils.js +3 -1
- package/lib/functions/secrets.js +1 -1
- package/lib/gcp/cloudfunctionsv2.js +3 -0
- package/lib/gcp/cloudsql/interactive.js +1 -1
- package/lib/init/features/dataconnect/sdk.js +28 -3
- package/lib/init/features/genkit/index.js +17 -9
- package/lib/management/apps.js +314 -7
- package/lib/profileReport.js +1 -1
- package/lib/prompt.js +10 -2
- package/package.json +14 -5
- package/templates/genkit/firebase.0.9.0.template +1 -1
- package/templates/genkit/firebase.1.0.0.template +66 -0
- package/templates/init/dataconnect/mutations.gql +9 -26
- package/templates/init/dataconnect/queries.gql +9 -14
- package/templates/init/dataconnect/schema.gql +28 -21
- package/templates/setup/web.js +0 -5
package/lib/management/apps.js
CHANGED
|
@@ -1,16 +1,248 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.deleteAppAndroidSha = exports.createAppAndroidSha = exports.listAppAndroidSha = exports.getAppConfig = exports.getAppConfigFile = exports.listFirebaseApps = exports.createWebApp = exports.createAndroidApp = exports.createIosApp = exports.getAppPlatform = exports.ShaCertificateType = exports.AppPlatform = exports.APP_LIST_PAGE_SIZE = void 0;
|
|
3
|
+
exports.findIntelligentPathForAndroid = exports.findIntelligentPathForIOS = exports.deleteAppAndroidSha = exports.createAppAndroidSha = exports.listAppAndroidSha = exports.getAppConfig = exports.writeConfigToFile = exports.getAppConfigFile = exports.listFirebaseApps = exports.createWebApp = exports.createAndroidApp = exports.createIosApp = exports.getAppPlatform = exports.ShaCertificateType = exports.AppPlatform = exports.getSdkConfig = exports.checkForApps = exports.getSdkOutputPath = exports.sdkInit = exports.getPlatform = exports.APP_LIST_PAGE_SIZE = void 0;
|
|
4
|
+
const fs = require("fs-extra");
|
|
5
|
+
const ora = require("ora");
|
|
6
|
+
const path = require("path");
|
|
4
7
|
const apiv2_1 = require("../apiv2");
|
|
5
8
|
const api_1 = require("../api");
|
|
6
9
|
const error_1 = require("../error");
|
|
7
10
|
const logger_1 = require("../logger");
|
|
8
11
|
const operation_poller_1 = require("../operation-poller");
|
|
9
|
-
const
|
|
12
|
+
const types_1 = require("../dataconnect/types");
|
|
13
|
+
const projectUtils_1 = require("../projectUtils");
|
|
14
|
+
const prompt_1 = require("../prompt");
|
|
15
|
+
const projects_1 = require("./projects");
|
|
16
|
+
const fileUtils_1 = require("../dataconnect/fileUtils");
|
|
17
|
+
const utils_1 = require("../utils");
|
|
10
18
|
const TIMEOUT_MILLIS = 30000;
|
|
11
19
|
exports.APP_LIST_PAGE_SIZE = 100;
|
|
12
20
|
const CREATE_APP_API_REQUEST_TIMEOUT_MILLIS = 15000;
|
|
13
|
-
const
|
|
21
|
+
const DISPLAY_NAME_QUESTION = {
|
|
22
|
+
type: "input",
|
|
23
|
+
name: "displayName",
|
|
24
|
+
default: "",
|
|
25
|
+
message: "What would you like to call your app?",
|
|
26
|
+
};
|
|
27
|
+
async function getPlatform(appDir, config) {
|
|
28
|
+
let targetPlatform = await (0, fileUtils_1.getPlatformFromFolder)(appDir);
|
|
29
|
+
if (targetPlatform === types_1.Platform.NONE) {
|
|
30
|
+
appDir = await (0, prompt_1.promptForDirectory)({
|
|
31
|
+
config,
|
|
32
|
+
relativeTo: appDir,
|
|
33
|
+
message: "We couldn't determine what kind of app you're using. Where is your app directory?",
|
|
34
|
+
});
|
|
35
|
+
targetPlatform = await (0, fileUtils_1.getPlatformFromFolder)(appDir);
|
|
36
|
+
}
|
|
37
|
+
if (targetPlatform === types_1.Platform.NONE || targetPlatform === types_1.Platform.MULTIPLE) {
|
|
38
|
+
if (targetPlatform === types_1.Platform.NONE) {
|
|
39
|
+
(0, utils_1.logBullet)(`Couldn't automatically detect app your in directory ${appDir}.`);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
(0, utils_1.logSuccess)(`Detected multiple app platforms in directory ${appDir}`);
|
|
43
|
+
}
|
|
44
|
+
const platforms = [
|
|
45
|
+
{ name: "iOS (Swift)", value: types_1.Platform.IOS },
|
|
46
|
+
{ name: "Web (JavaScript)", value: types_1.Platform.WEB },
|
|
47
|
+
{ name: "Android (Kotlin)", value: types_1.Platform.ANDROID },
|
|
48
|
+
];
|
|
49
|
+
targetPlatform = await (0, prompt_1.promptOnce)({
|
|
50
|
+
message: "Which platform do you want to set up an SDK for? Note: We currently do not support automatically setting up C++ or Unity projects.",
|
|
51
|
+
type: "list",
|
|
52
|
+
choices: platforms,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
else if (targetPlatform === types_1.Platform.FLUTTER) {
|
|
56
|
+
(0, utils_1.logWarning)(`Detected ${targetPlatform} app in directory ${appDir}`);
|
|
57
|
+
throw new error_1.FirebaseError(`Flutter is not supported by apps:configure.
|
|
58
|
+
Please follow the link below to set up firebase for your Flutter app:
|
|
59
|
+
https://firebase.google.com/docs/flutter/setup
|
|
60
|
+
`);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
(0, utils_1.logSuccess)(`Detected ${targetPlatform} app in directory ${appDir}`);
|
|
64
|
+
}
|
|
65
|
+
return targetPlatform === types_1.Platform.MULTIPLE
|
|
66
|
+
? AppPlatform.PLATFORM_UNSPECIFIED
|
|
67
|
+
: targetPlatform;
|
|
68
|
+
}
|
|
69
|
+
exports.getPlatform = getPlatform;
|
|
70
|
+
async function initiateIosAppCreation(options) {
|
|
71
|
+
if (!options.nonInteractive) {
|
|
72
|
+
await (0, prompt_1.prompt)(options, [
|
|
73
|
+
DISPLAY_NAME_QUESTION,
|
|
74
|
+
{
|
|
75
|
+
type: "input",
|
|
76
|
+
default: "",
|
|
77
|
+
name: "bundleId",
|
|
78
|
+
message: "Please specify your iOS app bundle ID:",
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
type: "input",
|
|
82
|
+
default: "",
|
|
83
|
+
name: "appStoreId",
|
|
84
|
+
message: "Please specify your iOS app App Store ID:",
|
|
85
|
+
},
|
|
86
|
+
]);
|
|
87
|
+
}
|
|
88
|
+
if (!options.bundleId) {
|
|
89
|
+
throw new error_1.FirebaseError("Bundle ID for iOS app cannot be empty");
|
|
90
|
+
}
|
|
91
|
+
const spinner = ora("Creating your iOS app").start();
|
|
92
|
+
try {
|
|
93
|
+
const appData = await createIosApp(options.project, {
|
|
94
|
+
displayName: options.displayName,
|
|
95
|
+
bundleId: options.bundleId,
|
|
96
|
+
appStoreId: options.appStoreId,
|
|
97
|
+
});
|
|
98
|
+
spinner.succeed();
|
|
99
|
+
return appData;
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
spinner.fail();
|
|
103
|
+
throw err;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async function initiateAndroidAppCreation(options) {
|
|
107
|
+
if (!options.nonInteractive) {
|
|
108
|
+
await (0, prompt_1.prompt)(options, [
|
|
109
|
+
DISPLAY_NAME_QUESTION,
|
|
110
|
+
{
|
|
111
|
+
type: "input",
|
|
112
|
+
default: "",
|
|
113
|
+
name: "packageName",
|
|
114
|
+
message: "Please specify your Android app package name:",
|
|
115
|
+
},
|
|
116
|
+
]);
|
|
117
|
+
}
|
|
118
|
+
if (!options.packageName) {
|
|
119
|
+
throw new error_1.FirebaseError("Package name for Android app cannot be empty");
|
|
120
|
+
}
|
|
121
|
+
const spinner = ora("Creating your Android app").start();
|
|
122
|
+
try {
|
|
123
|
+
const appData = await createAndroidApp(options.project, {
|
|
124
|
+
displayName: options.displayName,
|
|
125
|
+
packageName: options.packageName,
|
|
126
|
+
});
|
|
127
|
+
spinner.succeed();
|
|
128
|
+
return appData;
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
spinner.fail();
|
|
132
|
+
throw err;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
async function initiateWebAppCreation(options) {
|
|
136
|
+
if (!options.nonInteractive) {
|
|
137
|
+
await (0, prompt_1.prompt)(options, [DISPLAY_NAME_QUESTION]);
|
|
138
|
+
}
|
|
139
|
+
if (!options.displayName) {
|
|
140
|
+
throw new error_1.FirebaseError("Display name for Web app cannot be empty");
|
|
141
|
+
}
|
|
142
|
+
const spinner = ora("Creating your Web app").start();
|
|
143
|
+
try {
|
|
144
|
+
const appData = await createWebApp(options.project, { displayName: options.displayName });
|
|
145
|
+
spinner.succeed();
|
|
146
|
+
return appData;
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
spinner.fail();
|
|
150
|
+
throw err;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
async function sdkInit(appPlatform, options) {
|
|
154
|
+
let appData;
|
|
155
|
+
switch (appPlatform) {
|
|
156
|
+
case AppPlatform.IOS:
|
|
157
|
+
appData = await initiateIosAppCreation(options);
|
|
158
|
+
break;
|
|
159
|
+
case AppPlatform.ANDROID:
|
|
160
|
+
appData = await initiateAndroidAppCreation(options);
|
|
161
|
+
break;
|
|
162
|
+
case AppPlatform.WEB:
|
|
163
|
+
appData = await initiateWebAppCreation(options);
|
|
164
|
+
break;
|
|
165
|
+
default:
|
|
166
|
+
throw new error_1.FirebaseError("Unexpected error. This should not happen");
|
|
167
|
+
}
|
|
168
|
+
return appData;
|
|
169
|
+
}
|
|
170
|
+
exports.sdkInit = sdkInit;
|
|
171
|
+
async function getSdkOutputPath(appDir, platform, config) {
|
|
172
|
+
switch (platform) {
|
|
173
|
+
case AppPlatform.ANDROID:
|
|
174
|
+
const androidPath = await findIntelligentPathForAndroid(appDir, config);
|
|
175
|
+
return path.join(androidPath, "google-services.json");
|
|
176
|
+
case AppPlatform.WEB:
|
|
177
|
+
return path.join(appDir, "firebase-js-config.json");
|
|
178
|
+
case AppPlatform.IOS:
|
|
179
|
+
const iosPath = await findIntelligentPathForIOS(appDir, config);
|
|
180
|
+
return path.join(iosPath, "GoogleService-Info.plist");
|
|
181
|
+
}
|
|
182
|
+
throw new error_1.FirebaseError("Platform " + platform.toString() + " is not supported yet.");
|
|
183
|
+
}
|
|
184
|
+
exports.getSdkOutputPath = getSdkOutputPath;
|
|
185
|
+
function checkForApps(apps, appPlatform) {
|
|
186
|
+
if (!apps.length) {
|
|
187
|
+
throw new error_1.FirebaseError(`There are no ${appPlatform === AppPlatform.ANY ? "" : appPlatform + " "}apps ` +
|
|
188
|
+
"associated with this Firebase project");
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
exports.checkForApps = checkForApps;
|
|
192
|
+
async function selectAppInteractively(apps, appPlatform) {
|
|
193
|
+
checkForApps(apps, appPlatform);
|
|
194
|
+
const choices = apps.map((app) => {
|
|
195
|
+
return {
|
|
196
|
+
name: `${app.displayName || app.bundleId || app.packageName}` +
|
|
197
|
+
` - ${app.appId} (${app.platform})`,
|
|
198
|
+
value: app,
|
|
199
|
+
};
|
|
200
|
+
});
|
|
201
|
+
return await (0, prompt_1.promptOnce)({
|
|
202
|
+
type: "list",
|
|
203
|
+
message: `Select the ${appPlatform === AppPlatform.ANY ? "" : appPlatform + " "}` +
|
|
204
|
+
"app to get the configuration data:",
|
|
205
|
+
choices,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
async function getSdkConfig(options, appPlatform, appId) {
|
|
209
|
+
if (!appId) {
|
|
210
|
+
let projectId = (0, projectUtils_1.needProjectId)(options);
|
|
211
|
+
if (options.nonInteractive && !projectId) {
|
|
212
|
+
throw new error_1.FirebaseError("Must supply app and project ids in non-interactive mode.");
|
|
213
|
+
}
|
|
214
|
+
else if (!projectId) {
|
|
215
|
+
const result = await (0, projects_1.getOrPromptProject)(options);
|
|
216
|
+
projectId = result.projectId;
|
|
217
|
+
}
|
|
218
|
+
const apps = await listFirebaseApps(projectId, appPlatform);
|
|
219
|
+
checkForApps(apps, appPlatform);
|
|
220
|
+
if (apps.length === 1) {
|
|
221
|
+
appId = apps[0].appId;
|
|
222
|
+
appPlatform = apps[0].platform;
|
|
223
|
+
}
|
|
224
|
+
else if (options.nonInteractive) {
|
|
225
|
+
throw new error_1.FirebaseError(`Project ${projectId} has multiple apps, must specify an app id.`);
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
const appMetadata = await selectAppInteractively(apps, appPlatform);
|
|
229
|
+
appId = appMetadata.appId;
|
|
230
|
+
appPlatform = appMetadata.platform;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
let configData;
|
|
234
|
+
const spinner = ora(`Downloading configuration data for your Firebase ${appPlatform} app`).start();
|
|
235
|
+
try {
|
|
236
|
+
configData = await getAppConfig(appId, appPlatform);
|
|
237
|
+
}
|
|
238
|
+
catch (err) {
|
|
239
|
+
spinner.fail();
|
|
240
|
+
throw err;
|
|
241
|
+
}
|
|
242
|
+
spinner.succeed();
|
|
243
|
+
return configData;
|
|
244
|
+
}
|
|
245
|
+
exports.getSdkConfig = getSdkConfig;
|
|
14
246
|
var AppPlatform;
|
|
15
247
|
(function (AppPlatform) {
|
|
16
248
|
AppPlatform["PLATFORM_UNSPECIFIED"] = "PLATFORM_UNSPECIFIED";
|
|
@@ -182,13 +414,12 @@ function getAppConfigResourceString(appId, platform) {
|
|
|
182
414
|
}
|
|
183
415
|
function parseConfigFromResponse(responseBody, platform) {
|
|
184
416
|
if (platform === AppPlatform.WEB) {
|
|
185
|
-
const JS_TEMPLATE = (0, templates_1.readTemplateSync)("setup/web.js");
|
|
186
417
|
return {
|
|
187
|
-
fileName:
|
|
188
|
-
fileContents:
|
|
418
|
+
fileName: "firebase-js-config.json",
|
|
419
|
+
fileContents: JSON.stringify(responseBody, null, 2),
|
|
189
420
|
};
|
|
190
421
|
}
|
|
191
|
-
else if (
|
|
422
|
+
else if ("configFilename" in responseBody) {
|
|
192
423
|
return {
|
|
193
424
|
fileName: responseBody.configFilename,
|
|
194
425
|
fileContents: Buffer.from(responseBody.configFileContents, "base64").toString("utf8"),
|
|
@@ -200,6 +431,24 @@ function getAppConfigFile(config, platform) {
|
|
|
200
431
|
return parseConfigFromResponse(config, platform);
|
|
201
432
|
}
|
|
202
433
|
exports.getAppConfigFile = getAppConfigFile;
|
|
434
|
+
async function writeConfigToFile(filename, nonInteractive, fileContents) {
|
|
435
|
+
if (fs.existsSync(filename)) {
|
|
436
|
+
if (nonInteractive) {
|
|
437
|
+
throw new error_1.FirebaseError(`${filename} already exists`);
|
|
438
|
+
}
|
|
439
|
+
const overwrite = await (0, prompt_1.promptOnce)({
|
|
440
|
+
type: "confirm",
|
|
441
|
+
default: false,
|
|
442
|
+
message: `${filename} already exists. Do you want to overwrite?`,
|
|
443
|
+
});
|
|
444
|
+
if (!overwrite) {
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
await fs.writeFile(filename, fileContents);
|
|
449
|
+
return true;
|
|
450
|
+
}
|
|
451
|
+
exports.writeConfigToFile = writeConfigToFile;
|
|
203
452
|
async function getAppConfig(appId, platform) {
|
|
204
453
|
try {
|
|
205
454
|
const response = await apiClient.request({
|
|
@@ -278,3 +527,61 @@ async function deleteAppAndroidSha(projectId, appId, shaId) {
|
|
|
278
527
|
}
|
|
279
528
|
}
|
|
280
529
|
exports.deleteAppAndroidSha = deleteAppAndroidSha;
|
|
530
|
+
async function findIntelligentPathForIOS(appDir, options) {
|
|
531
|
+
const currentFiles = await fs.readdir(appDir, { withFileTypes: true });
|
|
532
|
+
for (let i = 0; i < currentFiles.length; i++) {
|
|
533
|
+
const dirent = currentFiles[i];
|
|
534
|
+
const xcodeStr = ".xcodeproj";
|
|
535
|
+
const file = dirent.name;
|
|
536
|
+
if (file.endsWith(xcodeStr)) {
|
|
537
|
+
return path.join(appDir, file.substring(0, file.length - xcodeStr.length));
|
|
538
|
+
}
|
|
539
|
+
else if (file === "Info.plist" ||
|
|
540
|
+
file === "Assets.xcassets" ||
|
|
541
|
+
(dirent.isDirectory() && file === "Preview Content")) {
|
|
542
|
+
return appDir;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
let outputPath = null;
|
|
546
|
+
if (!options.nonInteractive) {
|
|
547
|
+
outputPath = await (0, prompt_1.promptForDirectory)({
|
|
548
|
+
config: options.config,
|
|
549
|
+
message: `We weren't able to automatically determine the output directory. Where would you like to output your config file?`,
|
|
550
|
+
relativeTo: appDir,
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
if (!outputPath) {
|
|
554
|
+
throw new Error("We weren't able to automatically determine the output directory.");
|
|
555
|
+
}
|
|
556
|
+
return outputPath;
|
|
557
|
+
}
|
|
558
|
+
exports.findIntelligentPathForIOS = findIntelligentPathForIOS;
|
|
559
|
+
async function findIntelligentPathForAndroid(appDir, options) {
|
|
560
|
+
const paths = appDir.split("/");
|
|
561
|
+
if (paths[0] === "app") {
|
|
562
|
+
return appDir;
|
|
563
|
+
}
|
|
564
|
+
else {
|
|
565
|
+
const currentFiles = await fs.readdir(appDir, { withFileTypes: true });
|
|
566
|
+
const dirs = [];
|
|
567
|
+
for (const fileOrDir of currentFiles) {
|
|
568
|
+
if (fileOrDir.isDirectory()) {
|
|
569
|
+
if (fileOrDir.name === "src") {
|
|
570
|
+
return appDir;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
let module = path.join(appDir, "app");
|
|
575
|
+
if (dirs.length === 1) {
|
|
576
|
+
return module;
|
|
577
|
+
}
|
|
578
|
+
if (!options.nonInteractive) {
|
|
579
|
+
module = await (0, prompt_1.promptForDirectory)({
|
|
580
|
+
config: options.config,
|
|
581
|
+
message: `We weren't able to automatically determine the output directory. Where would you like to output your config file?`,
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
return module;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
exports.findIntelligentPathForAndroid = findIntelligentPathForAndroid;
|
package/lib/profileReport.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ProfileReport = exports.extractReadableIndex = exports.formatBytes = exports.formatNumber = exports.pathString = exports.extractJSON = void 0;
|
|
4
4
|
const clc = require("colorette");
|
|
5
|
-
const Table = require("cli-
|
|
5
|
+
const Table = require("cli-table3");
|
|
6
6
|
const fs = require("fs");
|
|
7
7
|
const _ = require("lodash");
|
|
8
8
|
const readline = require("readline");
|
package/lib/prompt.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.promptForDirectory = exports.confirm = exports.promptOnce = exports.prompt = void 0;
|
|
4
4
|
const inquirer = require("inquirer");
|
|
5
|
+
const path = require("path");
|
|
5
6
|
const fsutils_1 = require("./fsutils");
|
|
6
7
|
const error_1 = require("./error");
|
|
7
8
|
const logger_1 = require("./logger");
|
|
@@ -54,9 +55,16 @@ exports.confirm = confirm;
|
|
|
54
55
|
async function promptForDirectory(args) {
|
|
55
56
|
let dir = "";
|
|
56
57
|
while (!dir) {
|
|
57
|
-
const
|
|
58
|
+
const promptPath = await promptOnce({
|
|
58
59
|
message: args.message,
|
|
59
|
-
})
|
|
60
|
+
});
|
|
61
|
+
let target;
|
|
62
|
+
if (args.relativeTo) {
|
|
63
|
+
target = path.resolve(args.relativeTo, promptPath);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
target = args.config.path(promptPath);
|
|
67
|
+
}
|
|
60
68
|
if ((0, fsutils_1.fileExistsSync)(target)) {
|
|
61
69
|
logger_1.logger.error(`Expected a directory, but ${target} is a file. Please provide a path to a directory.`);
|
|
62
70
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "firebase-tools",
|
|
3
|
-
"version": "13.
|
|
3
|
+
"version": "13.31.0",
|
|
4
4
|
"description": "Command-Line Interface for Firebase",
|
|
5
5
|
"main": "./lib/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
],
|
|
30
30
|
"preferGlobal": true,
|
|
31
31
|
"engines": {
|
|
32
|
-
"node": ">=18.0.0 || >=20.0.0"
|
|
32
|
+
"node": ">=18.0.0 || >=20.0.0 || >=22.0.0"
|
|
33
33
|
},
|
|
34
34
|
"author": "Firebase (https://firebase.google.com/)",
|
|
35
35
|
"license": "MIT",
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
"body-parser": "^1.19.0",
|
|
72
72
|
"chokidar": "^3.6.0",
|
|
73
73
|
"cjson": "^0.3.1",
|
|
74
|
-
"cli-
|
|
74
|
+
"cli-table3": "0.6.5",
|
|
75
75
|
"colorette": "^2.0.19",
|
|
76
76
|
"commander": "^5.1.0",
|
|
77
77
|
"configstore": "^5.0.1",
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
"exegesis-express": "^4.0.0",
|
|
85
85
|
"express": "^4.16.4",
|
|
86
86
|
"filesize": "^6.1.0",
|
|
87
|
-
"form-data": "^4.0.
|
|
87
|
+
"form-data": "^4.0.1",
|
|
88
88
|
"fs-extra": "^10.1.0",
|
|
89
89
|
"fuzzy": "^0.1.3",
|
|
90
90
|
"gaxios": "^6.7.0",
|
|
@@ -116,7 +116,7 @@
|
|
|
116
116
|
"sql-formatter": "^15.3.0",
|
|
117
117
|
"stream-chain": "^2.2.4",
|
|
118
118
|
"stream-json": "^1.7.3",
|
|
119
|
-
"superstatic": "^9.
|
|
119
|
+
"superstatic": "^9.2.0",
|
|
120
120
|
"tar": "^6.1.11",
|
|
121
121
|
"tcp-port-used": "^1.0.2",
|
|
122
122
|
"tmp": "^0.2.3",
|
|
@@ -128,5 +128,14 @@
|
|
|
128
128
|
"winston-transport": "^4.4.0",
|
|
129
129
|
"ws": "^7.5.10",
|
|
130
130
|
"yaml": "^2.4.1"
|
|
131
|
+
},
|
|
132
|
+
"overrides": {
|
|
133
|
+
"@angular-devkit/core": {
|
|
134
|
+
"ajv-formats": "3.0.1",
|
|
135
|
+
"ajv": "^8.17.1"
|
|
136
|
+
},
|
|
137
|
+
"node-fetch": {
|
|
138
|
+
"whatwg-url": "^14.0.0"
|
|
139
|
+
}
|
|
131
140
|
}
|
|
132
141
|
}
|
|
@@ -19,7 +19,7 @@ export const menuSuggestionFlow = onFlow(
|
|
|
19
19
|
ai,
|
|
20
20
|
{
|
|
21
21
|
name: "menuSuggestionFlow",
|
|
22
|
-
inputSchema: z.string(),
|
|
22
|
+
inputSchema: z.string().describe("A restaurant theme").default("seafood"),
|
|
23
23
|
outputSchema: z.string(),
|
|
24
24
|
authPolicy: firebaseAuth((user) => {
|
|
25
25
|
// By default, the firebaseAuth policy requires that all requests have an
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Import the Genkit core libraries and plugins.
|
|
2
|
+
import {genkit, z} from "genkit";
|
|
3
|
+
$GENKIT_CONFIG_IMPORTS
|
|
4
|
+
$GENKIT_MODEL_IMPORT
|
|
5
|
+
|
|
6
|
+
// Cloud Functions for Firebase supports Genkit natively. The onCallGenkit function creates a callable
|
|
7
|
+
// function from a Genkit action. It automatically implements streaming if your flow does.
|
|
8
|
+
// The https library also has other utility methods such as hasClaim, which verifies that
|
|
9
|
+
// a caller's token has a specific claim (optionally matching a specific value)
|
|
10
|
+
import { onCallGenkit, hasClaim } from "firebase-functions/https";
|
|
11
|
+
|
|
12
|
+
// Genkit models generally depend on an API key. APIs should be stored in Cloud Secret Manager so that
|
|
13
|
+
// access to these sensitive values can be controlled. defineSecret does this for you automatically.
|
|
14
|
+
// If you are using Google generative AI you can get an API key at https://aistudio.google.com/app/apikey
|
|
15
|
+
import { defineSecret } from "firebase-functions/params";
|
|
16
|
+
const apiKey = defineSecret("GOOGLE_GENAI_API_KEY");
|
|
17
|
+
|
|
18
|
+
const ai = genkit({
|
|
19
|
+
plugins: [
|
|
20
|
+
$GENKIT_CONFIG_PLUGINS
|
|
21
|
+
],
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Define a simple flow that prompts an LLM to generate menu suggestions.
|
|
25
|
+
const menuSuggestionFlow = ai.defineFlow({
|
|
26
|
+
name: "menuSuggestionFlow",
|
|
27
|
+
inputSchema: z.string().describe("A restaurant theme").default("seafood"),
|
|
28
|
+
outputSchema: z.string(),
|
|
29
|
+
streamSchema: z.string(),
|
|
30
|
+
}, async (subject, { sendChunk }) => {
|
|
31
|
+
// Construct a request and send it to the model API.
|
|
32
|
+
const prompt =
|
|
33
|
+
`Suggest an item for the menu of a ${subject} themed restaurant`;
|
|
34
|
+
const { response, stream } = ai.generateStream({
|
|
35
|
+
model: $GENKIT_MODEL,
|
|
36
|
+
prompt: prompt,
|
|
37
|
+
config: {
|
|
38
|
+
temperature: 1,
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
for await (const chunk of stream) {
|
|
43
|
+
sendChunk(chunk.text);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Handle the response from the model API. In this sample, we just
|
|
47
|
+
// convert it to a string, but more complicated flows might coerce the
|
|
48
|
+
// response into structured output or chain the response into another
|
|
49
|
+
// LLM call, etc.
|
|
50
|
+
return (await response).text;
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
export const menuSuggestion = onCallGenkit({
|
|
55
|
+
// Uncomment to enable AppCheck. This can reduce costs by ensuring only your Verified
|
|
56
|
+
// app users can use your API. Read more at https://firebase.google.com/docs/app-check/cloud-functions
|
|
57
|
+
// enforceAppCheck: true,
|
|
58
|
+
|
|
59
|
+
// authPolicy can be any callback that accepts an AuthData (a uid and tokens dictionary) and the
|
|
60
|
+
// request data. The isSignedIn() and hasClaim() helpers can be used to simplify. The following
|
|
61
|
+
// will require the user to have the email_verified claim, for example.
|
|
62
|
+
// authPolicy: hasClaim("email_verified"),
|
|
63
|
+
|
|
64
|
+
// Grant access to the API key to this function:
|
|
65
|
+
secrets: [apiKey],
|
|
66
|
+
}, menuSuggestionFlow);
|
|
@@ -1,36 +1,20 @@
|
|
|
1
1
|
# # Example mutations for a simple movie app
|
|
2
2
|
|
|
3
3
|
# # Create a movie based on user input
|
|
4
|
-
# mutation CreateMovie(
|
|
5
|
-
#
|
|
6
|
-
# $genre:
|
|
7
|
-
# $imageUrl: String!
|
|
8
|
-
# ) @auth(level: USER_EMAIL_VERIFIED) {
|
|
9
|
-
# movie_insert(
|
|
10
|
-
# data: {
|
|
11
|
-
# title: $title
|
|
12
|
-
# genre: $genre
|
|
13
|
-
# imageUrl: $imageUrl
|
|
14
|
-
# }
|
|
15
|
-
# )
|
|
4
|
+
# mutation CreateMovie($title: String!, $genre: String!, $imageUrl: String!)
|
|
5
|
+
# @auth(level: USER_EMAIL_VERIFIED) {
|
|
6
|
+
# movie_insert(data: { title: $title, genre: $genre, imageUrl: $imageUrl })
|
|
16
7
|
# }
|
|
17
8
|
|
|
18
9
|
# # Upsert (update or insert) a user's username based on their auth.uid
|
|
19
10
|
# mutation UpsertUser($username: String!) @auth(level: USER) {
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
# id_expr: "auth.uid"
|
|
23
|
-
# username: $username
|
|
24
|
-
# }
|
|
25
|
-
# )
|
|
11
|
+
# # The "auth.uid" server value ensures that users can only register their own user.
|
|
12
|
+
# user_upsert(data: { id_expr: "auth.uid", username: $username })
|
|
26
13
|
# }
|
|
27
14
|
|
|
28
15
|
# # Add a review for a movie
|
|
29
|
-
# mutation AddReview(
|
|
30
|
-
#
|
|
31
|
-
# $rating: Int!
|
|
32
|
-
# $reviewText: String!
|
|
33
|
-
# ) @auth(level: USER) {
|
|
16
|
+
# mutation AddReview($movieId: UUID!, $rating: Int!, $reviewText: String!)
|
|
17
|
+
# @auth(level: USER) {
|
|
34
18
|
# review_upsert(
|
|
35
19
|
# data: {
|
|
36
20
|
# userId_expr: "auth.uid"
|
|
@@ -43,8 +27,7 @@
|
|
|
43
27
|
# }
|
|
44
28
|
|
|
45
29
|
# # Logged in user can delete their review for a movie
|
|
46
|
-
# mutation DeleteReview(
|
|
47
|
-
#
|
|
48
|
-
# ) @auth(level: USER) {
|
|
30
|
+
# mutation DeleteReview($movieId: UUID!) @auth(level: USER) {
|
|
31
|
+
# # The "auth.uid" server value ensures that users can only delete their own reviews.
|
|
49
32
|
# review_delete(key: { userId_expr: "auth.uid", movieId: $movieId })
|
|
50
33
|
# }
|
|
@@ -13,19 +13,21 @@
|
|
|
13
13
|
|
|
14
14
|
# # List all users, only admins should be able to list all users, so we use NO_ACCESS
|
|
15
15
|
# query ListUsers @auth(level: NO_ACCESS) {
|
|
16
|
-
# users {
|
|
16
|
+
# users {
|
|
17
|
+
# id
|
|
18
|
+
# username
|
|
19
|
+
# }
|
|
17
20
|
# }
|
|
18
21
|
|
|
19
|
-
# # Logged in
|
|
20
|
-
# # Since the query
|
|
22
|
+
# # Logged in users can list all their reviews and movie titles associated with the review
|
|
23
|
+
# # Since the query uses the uid of the current authenticated user, we set auth level to USER
|
|
21
24
|
# query ListUserReviews @auth(level: USER) {
|
|
22
|
-
# user(key: {id_expr: "auth.uid"}) {
|
|
25
|
+
# user(key: { id_expr: "auth.uid" }) {
|
|
23
26
|
# id
|
|
24
27
|
# username
|
|
25
28
|
# # <field>_on_<foreign_key_field> makes it easy to grab info from another table
|
|
26
29
|
# # Here, we use it to grab all the reviews written by the user.
|
|
27
30
|
# reviews: reviews_on_user {
|
|
28
|
-
# id
|
|
29
31
|
# rating
|
|
30
32
|
# reviewDate
|
|
31
33
|
# reviewText
|
|
@@ -50,7 +52,6 @@
|
|
|
50
52
|
# description
|
|
51
53
|
# }
|
|
52
54
|
# reviews: reviews_on_movie {
|
|
53
|
-
# id
|
|
54
55
|
# reviewText
|
|
55
56
|
# reviewDate
|
|
56
57
|
# rating
|
|
@@ -63,16 +64,10 @@
|
|
|
63
64
|
# }
|
|
64
65
|
|
|
65
66
|
# # Search for movies, actors, and reviews
|
|
66
|
-
# query SearchMovie(
|
|
67
|
-
# $titleInput: String
|
|
68
|
-
# $genre: String
|
|
69
|
-
# ) @auth(level: PUBLIC) {
|
|
67
|
+
# query SearchMovie($titleInput: String, $genre: String) @auth(level: PUBLIC) {
|
|
70
68
|
# movies(
|
|
71
69
|
# where: {
|
|
72
|
-
# _and: [
|
|
73
|
-
# { genre: { eq: $genre } }
|
|
74
|
-
# { title: { contains: $titleInput } }
|
|
75
|
-
# ]
|
|
70
|
+
# _and: [{ genre: { eq: $genre } }, { title: { contains: $titleInput } }]
|
|
76
71
|
# }
|
|
77
72
|
# ) {
|
|
78
73
|
# id
|