firebase-tools 15.8.1-migrateAngular.0 ā 15.9.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 +3 -3
- package/lib/apphosting/secrets/index.js +59 -0
- package/lib/commands/apphosting-secrets-set.js +2 -53
- package/lib/commands/studio-export.js +1 -1
- package/lib/dataconnect/schemaMigration.js +12 -1
- package/lib/emulator/downloadableEmulatorInfo.json +24 -24
- package/lib/experiments.js +1 -1
- package/lib/firebase_studio/migrate.js +62 -49
- package/lib/gcp/cloudsql/cloudsqladmin.js +8 -3
- package/lib/mcp/prompts/apptesting/run_test.js +6 -5
- package/lib/mcp/tools/apptesting/tests.js +1 -1
- package/package.json +1 -1
- package/templates/firebase-studio-export/readme_template.md +1 -1
- package/templates/init/functions/javascript/package-ongraphrequest.lint.json +1 -1
- package/templates/init/functions/javascript/package-ongraphrequest.nolint.json +1 -1
- package/templates/init/functions/typescript/package-ongraphrequest.lint.json +1 -1
- package/templates/init/functions/typescript/package-ongraphrequest.nolint.json +1 -1
|
@@ -71,7 +71,7 @@ exports.WARN_NO_BACKENDS = "To use this secret, your backend's service account m
|
|
|
71
71
|
"It does not look like you have a backend yet. After creating a backend, grant access with " +
|
|
72
72
|
clc.bold("firebase apphosting:secrets:grantaccess");
|
|
73
73
|
exports.GRANT_ACCESS_IN_FUTURE = `To grant access in the future, run ${clc.bold("firebase apphosting:secrets:grantaccess")}`;
|
|
74
|
-
async function selectBackendServiceAccounts(projectNumber, projectId,
|
|
74
|
+
async function selectBackendServiceAccounts(projectNumber, projectId, nonInteractive) {
|
|
75
75
|
const listBackends = await apphosting.listBackends(projectId, "-");
|
|
76
76
|
if (listBackends.unreachable.length) {
|
|
77
77
|
utils.logWarning(`Could not reach location(s) ${listBackends.unreachable.join(", ")}. You may need to run ` +
|
|
@@ -83,7 +83,7 @@ async function selectBackendServiceAccounts(projectNumber, projectId, options) {
|
|
|
83
83
|
}
|
|
84
84
|
if (listBackends.backends.length === 1) {
|
|
85
85
|
const grant = await prompt.confirm({
|
|
86
|
-
nonInteractive
|
|
86
|
+
nonInteractive,
|
|
87
87
|
default: true,
|
|
88
88
|
message: "To use this secret, your backend's service account must be granted access. Would you like to grant access now?",
|
|
89
89
|
});
|
|
@@ -101,7 +101,7 @@ async function selectBackendServiceAccounts(projectNumber, projectId, options) {
|
|
|
101
101
|
serviceAccountDisplay(metadata[0]) +
|
|
102
102
|
".\nGranting access to one backend will grant access to all backends.");
|
|
103
103
|
const grant = await prompt.confirm({
|
|
104
|
-
nonInteractive
|
|
104
|
+
nonInteractive,
|
|
105
105
|
default: true,
|
|
106
106
|
message: "Would you like to grant access to all backends now?",
|
|
107
107
|
});
|
|
@@ -7,6 +7,8 @@ exports.grantEmailsSecretAccess = grantEmailsSecretAccess;
|
|
|
7
7
|
exports.upsertSecret = upsertSecret;
|
|
8
8
|
exports.fetchSecrets = fetchSecrets;
|
|
9
9
|
exports.getSecretNameParts = getSecretNameParts;
|
|
10
|
+
exports.apphostingSecretsSetAction = apphostingSecretsSetAction;
|
|
11
|
+
const clc = require("colorette");
|
|
10
12
|
const error_1 = require("../../error");
|
|
11
13
|
const gcsm = require("../../gcp/secretManager");
|
|
12
14
|
const gcb = require("../../gcp/cloudbuild");
|
|
@@ -16,6 +18,9 @@ const secretManager_1 = require("../../gcp/secretManager");
|
|
|
16
18
|
const secretManager_2 = require("../../gcp/secretManager");
|
|
17
19
|
const utils = require("../../utils");
|
|
18
20
|
const prompt = require("../../prompt");
|
|
21
|
+
const dialogs = require("../../apphosting/secrets/dialogs");
|
|
22
|
+
const config = require("../../apphosting/config");
|
|
23
|
+
const projects_1 = require("../../management/projects");
|
|
19
24
|
function toMulti(accounts) {
|
|
20
25
|
const m = {
|
|
21
26
|
buildServiceAccounts: [accounts.buildServiceAccount],
|
|
@@ -169,3 +174,57 @@ function getSecretNameParts(secret) {
|
|
|
169
174
|
}
|
|
170
175
|
return [name, version];
|
|
171
176
|
}
|
|
177
|
+
async function apphostingSecretsSetAction(secretName, projectId, projectNumber, location, dataFile, nonInteractive) {
|
|
178
|
+
if (!projectNumber) {
|
|
179
|
+
projectNumber = (await (0, projects_1.getProject)(projectId)).projectNumber;
|
|
180
|
+
}
|
|
181
|
+
const created = await upsertSecret(projectId, secretName, location);
|
|
182
|
+
if (created === null) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
else if (created) {
|
|
186
|
+
utils.logSuccess(`Created new secret projects/${projectId}/secrets/${secretName}`);
|
|
187
|
+
}
|
|
188
|
+
const secretValue = await utils.readSecretValue(`Enter a value for ${secretName}`, dataFile);
|
|
189
|
+
const version = await gcsm.addVersion(projectId, secretName, secretValue);
|
|
190
|
+
utils.logSuccess(`Created new secret version ${gcsm.toSecretVersionResourceName(version)}`);
|
|
191
|
+
utils.logBullet(`You can access the contents of the secret's latest value with ${clc.bold(`firebase apphosting:secrets:access ${secretName}\n`)}`);
|
|
192
|
+
if (!created) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const type = await prompt.select({
|
|
196
|
+
message: "Is this secret for production or only local testing?",
|
|
197
|
+
choices: [
|
|
198
|
+
{ name: "Production", value: "production" },
|
|
199
|
+
{ name: "Local testing only", value: "local" },
|
|
200
|
+
],
|
|
201
|
+
nonInteractive: !!nonInteractive,
|
|
202
|
+
default: "production",
|
|
203
|
+
});
|
|
204
|
+
if (type === "local") {
|
|
205
|
+
const emailList = await prompt.input({
|
|
206
|
+
message: "Please enter a comma separated list of user or groups who should have access to this secret:",
|
|
207
|
+
});
|
|
208
|
+
if (emailList.length) {
|
|
209
|
+
await grantEmailsSecretAccess(projectId, [secretName], emailList.split(","));
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
utils.logBullet("To grant access in the future run " +
|
|
213
|
+
clc.bold(`firebase apphosting:secrets:grantaccess ${secretName} --emails [email list]`));
|
|
214
|
+
}
|
|
215
|
+
await config.maybeAddSecretToYaml(secretName, config.APPHOSTING_EMULATORS_YAML_FILE);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
const accounts = await dialogs.selectBackendServiceAccounts(projectNumber, projectId, !!nonInteractive);
|
|
219
|
+
if (!accounts.buildServiceAccounts.length && !accounts.runServiceAccounts.length) {
|
|
220
|
+
utils.logWarning(`To use this secret in your backend, you must grant access. You can do so in the future with ${clc.bold("firebase apphosting:secrets:grantaccess")}`);
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
await grantSecretAccess(projectId, projectNumber, secretName, accounts);
|
|
224
|
+
}
|
|
225
|
+
await config.maybeAddSecretToYaml(secretName, config.APPHOSTING_BASE_YAML_FILE);
|
|
226
|
+
utils.logBullet("To grant additional users access to this secret run " +
|
|
227
|
+
clc.bold(`firebase apphosting:secrets:grantaccess ${secretName} --email [email list]`) +
|
|
228
|
+
".\nTo grant additional backends access to this secret run " +
|
|
229
|
+
clc.bold(`firebase apphosting:secrets:grantaccess ${secretName} --backend [backend ID]`));
|
|
230
|
+
}
|
|
@@ -1,18 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.command = void 0;
|
|
4
|
-
const clc = require("colorette");
|
|
5
4
|
const command_1 = require("../command");
|
|
6
5
|
const projectUtils_1 = require("../projectUtils");
|
|
7
6
|
const requireAuth_1 = require("../requireAuth");
|
|
8
7
|
const gcsm = require("../gcp/secretManager");
|
|
9
8
|
const apphosting = require("../gcp/apphosting");
|
|
10
9
|
const requirePermissions_1 = require("../requirePermissions");
|
|
11
|
-
const
|
|
12
|
-
const dialogs = require("../apphosting/secrets/dialogs");
|
|
13
|
-
const config = require("../apphosting/config");
|
|
14
|
-
const utils = require("../utils");
|
|
15
|
-
const prompt = require("../prompt");
|
|
10
|
+
const secrets_1 = require("../apphosting/secrets");
|
|
16
11
|
exports.command = new command_1.Command("apphosting:secrets:set <secretName>")
|
|
17
12
|
.description("create or update a secret for use in Firebase App Hosting")
|
|
18
13
|
.option("-l, --location <location>", "optional location to retrict secret replication")
|
|
@@ -32,51 +27,5 @@ exports.command = new command_1.Command("apphosting:secrets:set <secretName>")
|
|
|
32
27
|
.action(async (secretName, options) => {
|
|
33
28
|
const projectId = (0, projectUtils_1.needProjectId)(options);
|
|
34
29
|
const projectNumber = await (0, projectUtils_1.needProjectNumber)(options);
|
|
35
|
-
|
|
36
|
-
if (created === null) {
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
else if (created) {
|
|
40
|
-
utils.logSuccess(`Created new secret projects/${projectId}/secrets/${secretName}`);
|
|
41
|
-
}
|
|
42
|
-
const secretValue = await utils.readSecretValue(`Enter a value for ${secretName}`, options.dataFile);
|
|
43
|
-
const version = await gcsm.addVersion(projectId, secretName, secretValue);
|
|
44
|
-
utils.logSuccess(`Created new secret version ${gcsm.toSecretVersionResourceName(version)}`);
|
|
45
|
-
utils.logBullet(`You can access the contents of the secret's latest value with ${clc.bold(`firebase apphosting:secrets:access ${secretName}\n`)}`);
|
|
46
|
-
if (!created) {
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
const type = await prompt.select({
|
|
50
|
-
message: "Is this secret for production or only local testing?",
|
|
51
|
-
choices: [
|
|
52
|
-
{ name: "Production", value: "production" },
|
|
53
|
-
{ name: "Local testing only", value: "local" },
|
|
54
|
-
],
|
|
55
|
-
});
|
|
56
|
-
if (type === "local") {
|
|
57
|
-
const emailList = await prompt.input({
|
|
58
|
-
message: "Please enter a comma separated list of user or groups who should have access to this secret:",
|
|
59
|
-
});
|
|
60
|
-
if (emailList.length) {
|
|
61
|
-
await secrets.grantEmailsSecretAccess(projectId, [secretName], emailList.split(","));
|
|
62
|
-
}
|
|
63
|
-
else {
|
|
64
|
-
utils.logBullet("To grant access in the future run " +
|
|
65
|
-
clc.bold(`firebase apphosting:secrets:grantaccess ${secretName} --emails [email list]`));
|
|
66
|
-
}
|
|
67
|
-
await config.maybeAddSecretToYaml(secretName, config.APPHOSTING_EMULATORS_YAML_FILE);
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
const accounts = await dialogs.selectBackendServiceAccounts(projectNumber, projectId, options);
|
|
71
|
-
if (!accounts.buildServiceAccounts.length && !accounts.runServiceAccounts.length) {
|
|
72
|
-
utils.logWarning(`To use this secret in your backend, you must grant access. You can do so in the future with ${clc.bold("firebase apphosting:secrets:grantaccess")}`);
|
|
73
|
-
}
|
|
74
|
-
else {
|
|
75
|
-
await secrets.grantSecretAccess(projectId, projectNumber, secretName, accounts);
|
|
76
|
-
}
|
|
77
|
-
await config.maybeAddSecretToYaml(secretName, config.APPHOSTING_BASE_YAML_FILE);
|
|
78
|
-
utils.logBullet("To grant additional users access to this secret run " +
|
|
79
|
-
clc.bold(`firebase apphosting:secrets:grantaccess ${secretName} --email [email list]`) +
|
|
80
|
-
".\nTo grant additional backends access to this secret run " +
|
|
81
|
-
clc.bold(`firebase apphosting:secrets:grantaccess ${secretName} --backend [backend ID]`));
|
|
30
|
+
return (0, secrets_1.apphostingSecretsSetAction)(secretName, projectId, projectNumber, options.location, options.dataFile, options.nonInteractive);
|
|
82
31
|
});
|
|
@@ -17,5 +17,5 @@ exports.command = new command_1.Command("studio:export <path>")
|
|
|
17
17
|
}
|
|
18
18
|
const rootPath = path.resolve(exportPath);
|
|
19
19
|
logger_1.logger.info(`Exporting Studio apps from ${rootPath} to Antigravity...`);
|
|
20
|
-
await (0, migrate_1.migrate)(rootPath,
|
|
20
|
+
await (0, migrate_1.migrate)(rootPath, options);
|
|
21
21
|
});
|
|
@@ -24,6 +24,7 @@ const cloudSqlAdminClient = require("../gcp/cloudsql/cloudsqladmin");
|
|
|
24
24
|
const errors = require("./errors");
|
|
25
25
|
const provisionCloudSql_1 = require("./provisionCloudSql");
|
|
26
26
|
const requireAuth_1 = require("../requireAuth");
|
|
27
|
+
const cloudbilling_1 = require("../gcp/cloudbilling");
|
|
27
28
|
async function setupSchemaIfNecessary(instanceId, databaseId, options) {
|
|
28
29
|
try {
|
|
29
30
|
await (0, connect_1.setupIAMUsers)(instanceId, options);
|
|
@@ -429,7 +430,17 @@ async function ensureServiceIsConnectedToCloudSql(serviceName, instanceName, dat
|
|
|
429
430
|
postgresql?.schemaValidation === "NONE") {
|
|
430
431
|
const [, , , , , serviceId] = serviceName.split("/");
|
|
431
432
|
const [, projectId, , , , instanceId] = postgresql.cloudSql.instance.split("/");
|
|
432
|
-
|
|
433
|
+
let isFreeTrial = false;
|
|
434
|
+
let billingEnabled = false;
|
|
435
|
+
try {
|
|
436
|
+
const instance = await cloudSqlAdminClient.getInstance(projectId, instanceId);
|
|
437
|
+
isFreeTrial = instance.settings?.userLabels?.["firebase-data-connect"] === "ft";
|
|
438
|
+
billingEnabled = await (0, cloudbilling_1.checkBillingEnabled)(projectId);
|
|
439
|
+
}
|
|
440
|
+
catch (err) {
|
|
441
|
+
}
|
|
442
|
+
throw new error_1.FirebaseError(`While checking the service ${serviceId}, ` +
|
|
443
|
+
(0, provisionCloudSql_1.cloudSQLBeingCreated)(projectId, instanceId, isFreeTrial, billingEnabled));
|
|
433
444
|
}
|
|
434
445
|
if (!currentSchema || !postgresql) {
|
|
435
446
|
if (!linkIfNotConnected) {
|
|
@@ -54,36 +54,36 @@
|
|
|
54
54
|
},
|
|
55
55
|
"dataconnect": {
|
|
56
56
|
"darwin": {
|
|
57
|
-
"version": "3.2.
|
|
58
|
-
"expectedSize":
|
|
59
|
-
"expectedChecksum": "
|
|
60
|
-
"expectedChecksumSHA256": "
|
|
61
|
-
"remoteUrl": "https://storage.googleapis.com/firemat-preview-drop/emulator/dataconnect-emulator-macos-amd64-v3.2.
|
|
62
|
-
"downloadPathRelativeToCacheDir": "dataconnect-emulator-3.2.
|
|
57
|
+
"version": "3.2.1",
|
|
58
|
+
"expectedSize": 30880608,
|
|
59
|
+
"expectedChecksum": "ef63443e5132e4ade7b89506fea60e29",
|
|
60
|
+
"expectedChecksumSHA256": "0a9c05413cb3048a4bef1968e55109ca67eb607e6d77355741cb9a7dcae51607",
|
|
61
|
+
"remoteUrl": "https://storage.googleapis.com/firemat-preview-drop/emulator/dataconnect-emulator-macos-amd64-v3.2.1",
|
|
62
|
+
"downloadPathRelativeToCacheDir": "dataconnect-emulator-3.2.1"
|
|
63
63
|
},
|
|
64
64
|
"darwin_arm64": {
|
|
65
|
-
"version": "3.2.
|
|
66
|
-
"expectedSize":
|
|
67
|
-
"expectedChecksum": "
|
|
68
|
-
"expectedChecksumSHA256": "
|
|
69
|
-
"remoteUrl": "https://storage.googleapis.com/firemat-preview-drop/emulator/dataconnect-emulator-macos-arm64-v3.2.
|
|
70
|
-
"downloadPathRelativeToCacheDir": "dataconnect-emulator-3.2.
|
|
65
|
+
"version": "3.2.1",
|
|
66
|
+
"expectedSize": 30339010,
|
|
67
|
+
"expectedChecksum": "866989b28ae5cad1084403bd4404dcee",
|
|
68
|
+
"expectedChecksumSHA256": "558e334c456c646b1ddf4b32ab43d976c5b03c191a4c92855e8c93cd32e1215a",
|
|
69
|
+
"remoteUrl": "https://storage.googleapis.com/firemat-preview-drop/emulator/dataconnect-emulator-macos-arm64-v3.2.1",
|
|
70
|
+
"downloadPathRelativeToCacheDir": "dataconnect-emulator-3.2.1"
|
|
71
71
|
},
|
|
72
72
|
"win32": {
|
|
73
|
-
"version": "3.2.
|
|
74
|
-
"expectedSize":
|
|
75
|
-
"expectedChecksum": "
|
|
76
|
-
"expectedChecksumSHA256": "
|
|
77
|
-
"remoteUrl": "https://storage.googleapis.com/firemat-preview-drop/emulator/dataconnect-emulator-windows-amd64-v3.2.
|
|
78
|
-
"downloadPathRelativeToCacheDir": "dataconnect-emulator-3.2.
|
|
73
|
+
"version": "3.2.1",
|
|
74
|
+
"expectedSize": 31385088,
|
|
75
|
+
"expectedChecksum": "cb46949156c889d5724f7fa80e8a613b",
|
|
76
|
+
"expectedChecksumSHA256": "1dd1f9ef7910d8295baac61741fde29fac536d82ff841a2d5cefe25e98f7cd98",
|
|
77
|
+
"remoteUrl": "https://storage.googleapis.com/firemat-preview-drop/emulator/dataconnect-emulator-windows-amd64-v3.2.1",
|
|
78
|
+
"downloadPathRelativeToCacheDir": "dataconnect-emulator-3.2.1.exe"
|
|
79
79
|
},
|
|
80
80
|
"linux": {
|
|
81
|
-
"version": "3.2.
|
|
82
|
-
"expectedSize":
|
|
83
|
-
"expectedChecksum": "
|
|
84
|
-
"expectedChecksumSHA256": "
|
|
85
|
-
"remoteUrl": "https://storage.googleapis.com/firemat-preview-drop/emulator/dataconnect-emulator-linux-amd64-v3.2.
|
|
86
|
-
"downloadPathRelativeToCacheDir": "dataconnect-emulator-3.2.
|
|
81
|
+
"version": "3.2.1",
|
|
82
|
+
"expectedSize": 30798008,
|
|
83
|
+
"expectedChecksum": "8c29b6b526efcbf13de278bd3b5b598a",
|
|
84
|
+
"expectedChecksumSHA256": "d7eb7f7afd98dd9018039c83782312f975b160c3708eb49f8fecdf7d27508d82",
|
|
85
|
+
"remoteUrl": "https://storage.googleapis.com/firemat-preview-drop/emulator/dataconnect-emulator-linux-amd64-v3.2.1",
|
|
86
|
+
"downloadPathRelativeToCacheDir": "dataconnect-emulator-3.2.1"
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
}
|
package/lib/experiments.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.extractMetadata = extractMetadata;
|
|
4
|
+
exports.uploadSecrets = uploadSecrets;
|
|
3
5
|
exports.migrate = migrate;
|
|
4
6
|
const fs = require("fs/promises");
|
|
5
7
|
const path = require("path");
|
|
@@ -10,7 +12,8 @@ const prompt = require("../prompt");
|
|
|
10
12
|
const apphosting = require("../gcp/apphosting");
|
|
11
13
|
const utils = require("../utils");
|
|
12
14
|
const templates_1 = require("../templates");
|
|
13
|
-
const
|
|
15
|
+
const secrets_1 = require("../apphosting/secrets");
|
|
16
|
+
const env = require("../functions/env");
|
|
14
17
|
async function downloadGitHubDir(apiUrl, localPath) {
|
|
15
18
|
const response = await fetch(apiUrl);
|
|
16
19
|
if (!response.ok) {
|
|
@@ -32,7 +35,7 @@ async function downloadGitHubDir(apiUrl, localPath) {
|
|
|
32
35
|
}
|
|
33
36
|
}
|
|
34
37
|
}
|
|
35
|
-
async function extractMetadata(rootPath) {
|
|
38
|
+
async function extractMetadata(rootPath, overrideProjectId) {
|
|
36
39
|
const metadataPath = path.join(rootPath, "metadata.json");
|
|
37
40
|
let metadata = {};
|
|
38
41
|
try {
|
|
@@ -42,7 +45,7 @@ async function extractMetadata(rootPath) {
|
|
|
42
45
|
catch (err) {
|
|
43
46
|
logger_1.logger.debug(`Could not read metadata.json at ${metadataPath}: ${err}`);
|
|
44
47
|
}
|
|
45
|
-
let projectId = metadata.projectId;
|
|
48
|
+
let projectId = overrideProjectId || metadata.projectId;
|
|
46
49
|
if (!projectId) {
|
|
47
50
|
try {
|
|
48
51
|
const firebasercContent = await fs.readFile(path.join(rootPath, ".firebaserc"), "utf8");
|
|
@@ -77,17 +80,13 @@ async function extractMetadata(rootPath) {
|
|
|
77
80
|
}
|
|
78
81
|
return { projectId, appName, blueprintContent };
|
|
79
82
|
}
|
|
80
|
-
async function updateReadme(rootPath, blueprintContent, appName
|
|
83
|
+
async function updateReadme(rootPath, blueprintContent, appName) {
|
|
81
84
|
const readmePath = path.join(rootPath, "README.md");
|
|
82
85
|
const readmeTemplate = await (0, templates_1.readTemplate)("firebase-studio-export/readme_template.md");
|
|
83
|
-
const startCommand = framework === "angular" ? "npm run start" : "npm run dev";
|
|
84
|
-
const localUrl = framework === "angular" ? "http://localhost:4200" : "http://localhost:9002";
|
|
85
86
|
const newReadme = readmeTemplate
|
|
86
87
|
.replace(/\${appName}/g, appName)
|
|
87
88
|
.replace("${exportDate}", new Date().toISOString().split("T")[0])
|
|
88
|
-
.replace("${blueprintContent}", blueprintContent.replace(/# \*\*App Name\*\*: .*/, "").trim())
|
|
89
|
-
.replace("${startCommand}", startCommand)
|
|
90
|
-
.replace("${localUrl}", localUrl);
|
|
89
|
+
.replace("${blueprintContent}", blueprintContent.replace(/# \*\*App Name\*\*: .*/, "").trim());
|
|
91
90
|
await fs.writeFile(readmePath, newReadme);
|
|
92
91
|
logger_1.logger.info("ā
Updated README.md with project details and origin info");
|
|
93
92
|
}
|
|
@@ -147,7 +146,10 @@ async function injectAgyContext(rootPath, projectId, appName) {
|
|
|
147
146
|
logger_1.logger.debug(`Could not read or write startup workflow: ${err}`);
|
|
148
147
|
}
|
|
149
148
|
}
|
|
150
|
-
async function assertSystemState() {
|
|
149
|
+
async function assertSystemState(startAgy) {
|
|
150
|
+
if (startAgy === false) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
151
153
|
try {
|
|
152
154
|
(0, child_process_1.execSync)("agy --version", { stdio: "ignore" });
|
|
153
155
|
logger_1.logger.info("ā
Antigravity IDE CLI (agy) detected");
|
|
@@ -214,7 +216,7 @@ async function createFirebaseConfigs(rootPath, projectId) {
|
|
|
214
216
|
logger_1.logger.info(`ā
Created firebase.json with backendId: ${backendId}`);
|
|
215
217
|
}
|
|
216
218
|
}
|
|
217
|
-
async function writeAgyConfigs(rootPath
|
|
219
|
+
async function writeAgyConfigs(rootPath) {
|
|
218
220
|
const vscodeDir = path.join(rootPath, ".vscode");
|
|
219
221
|
await fs.mkdir(vscodeDir, { recursive: true });
|
|
220
222
|
const tasksJson = {
|
|
@@ -250,32 +252,19 @@ async function writeAgyConfigs(rootPath, framework) {
|
|
|
250
252
|
logger_1.logger.info("ā
Updated .vscode/settings.json with startup preferences");
|
|
251
253
|
const launchJson = {
|
|
252
254
|
version: "0.2.0",
|
|
253
|
-
configurations: [
|
|
255
|
+
configurations: [
|
|
256
|
+
{
|
|
257
|
+
type: "node",
|
|
258
|
+
request: "launch",
|
|
259
|
+
name: "Next.js: debug server-side",
|
|
260
|
+
runtimeExecutable: "npm",
|
|
261
|
+
runtimeArgs: ["run", "dev"],
|
|
262
|
+
port: 9002,
|
|
263
|
+
console: "integratedTerminal",
|
|
264
|
+
preLaunchTask: "npm-install",
|
|
265
|
+
},
|
|
266
|
+
],
|
|
254
267
|
};
|
|
255
|
-
if (framework === "angular") {
|
|
256
|
-
launchJson.configurations.push({
|
|
257
|
-
type: "node",
|
|
258
|
-
request: "launch",
|
|
259
|
-
name: "Angular: debug server-side",
|
|
260
|
-
runtimeExecutable: "npm",
|
|
261
|
-
runtimeArgs: ["run", "start"],
|
|
262
|
-
port: 4200,
|
|
263
|
-
console: "integratedTerminal",
|
|
264
|
-
preLaunchTask: "npm-install",
|
|
265
|
-
});
|
|
266
|
-
}
|
|
267
|
-
else {
|
|
268
|
-
launchJson.configurations.push({
|
|
269
|
-
type: "node",
|
|
270
|
-
request: "launch",
|
|
271
|
-
name: "Next.js: debug server-side",
|
|
272
|
-
runtimeExecutable: "npm",
|
|
273
|
-
runtimeArgs: ["run", "dev"],
|
|
274
|
-
port: 9002,
|
|
275
|
-
console: "integratedTerminal",
|
|
276
|
-
preLaunchTask: "npm-install",
|
|
277
|
-
});
|
|
278
|
-
}
|
|
279
268
|
await fs.writeFile(path.join(vscodeDir, "launch.json"), JSON.stringify(launchJson, null, 2));
|
|
280
269
|
logger_1.logger.info("ā
Created .vscode/launch.json");
|
|
281
270
|
}
|
|
@@ -316,8 +305,36 @@ async function cleanupUnusedFiles(rootPath) {
|
|
|
316
305
|
logger_1.logger.debug(`Could not delete ${modifiedPath}: ${err}`);
|
|
317
306
|
}
|
|
318
307
|
}
|
|
319
|
-
async function
|
|
320
|
-
if (
|
|
308
|
+
async function uploadSecrets(rootPath, projectId) {
|
|
309
|
+
if (!projectId) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
const envPath = path.join(rootPath, ".env");
|
|
313
|
+
try {
|
|
314
|
+
await fs.access(envPath);
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
try {
|
|
320
|
+
const envContent = await fs.readFile(envPath, "utf8");
|
|
321
|
+
const parsedEnv = env.parse(envContent);
|
|
322
|
+
const geminiApiKey = parsedEnv.envs["GEMINI_API_KEY"];
|
|
323
|
+
if (geminiApiKey && geminiApiKey.trim().length > 0) {
|
|
324
|
+
logger_1.logger.info("ā³ Uploading GEMINI_API_KEY from .env to App Hosting secrets...");
|
|
325
|
+
await (0, secrets_1.apphostingSecretsSetAction)("GEMINI_API_KEY", projectId, undefined, undefined, envPath, true);
|
|
326
|
+
logger_1.logger.info("ā
Uploaded GEMINI_API_KEY secret");
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
logger_1.logger.debug("Skipping GEMINI_API_KEY upload: key is missing or blank in .env");
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
catch (err) {
|
|
333
|
+
utils.logWarning(`Failed to upload GEMINI_API_KEY secret: ${err}`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
async function askToOpenAntigravity(rootPath, appName, startAgy) {
|
|
337
|
+
if (startAgy === false) {
|
|
321
338
|
logger_1.logger.info('\nš Next steps: Open this folder in Antigravity and run the "Initial Project Setup" workflow.');
|
|
322
339
|
return;
|
|
323
340
|
}
|
|
@@ -343,23 +360,19 @@ async function askToOpenAntigravity(rootPath, appName, noStartAgyFlag) {
|
|
|
343
360
|
logger_1.logger.info('\nš Next steps: Open this folder in Antigravity and run the "Initial Project Setup" workflow.');
|
|
344
361
|
}
|
|
345
362
|
}
|
|
346
|
-
async function migrate(rootPath, options = {
|
|
363
|
+
async function migrate(rootPath, options = { startAgy: true }) {
|
|
347
364
|
logger_1.logger.info("š Starting Firebase Studio to Antigravity migration...");
|
|
348
|
-
await assertSystemState();
|
|
349
|
-
const { projectId, appName, blueprintContent } = await extractMetadata(rootPath);
|
|
350
|
-
|
|
351
|
-
const framework = discovery?.framework;
|
|
352
|
-
if (framework) {
|
|
353
|
-
logger_1.logger.info(`ā
Detected framework: ${framework}`);
|
|
354
|
-
}
|
|
355
|
-
await updateReadme(rootPath, blueprintContent, appName, framework);
|
|
365
|
+
await assertSystemState(options.startAgy);
|
|
366
|
+
const { projectId, appName, blueprintContent } = await extractMetadata(rootPath, options.project);
|
|
367
|
+
await updateReadme(rootPath, blueprintContent, appName);
|
|
356
368
|
await createFirebaseConfigs(rootPath, projectId);
|
|
369
|
+
await uploadSecrets(rootPath, projectId);
|
|
357
370
|
await injectAgyContext(rootPath, projectId, appName);
|
|
358
|
-
await writeAgyConfigs(rootPath
|
|
371
|
+
await writeAgyConfigs(rootPath);
|
|
359
372
|
await cleanupUnusedFiles(rootPath);
|
|
360
373
|
const currentFolderName = path.basename(rootPath);
|
|
361
374
|
if (currentFolderName === "download") {
|
|
362
375
|
logger_1.logger.info(`\nš” Tip: You might want to rename this folder to "${appName.toLowerCase().replace(/\s+/g, "-")}"`);
|
|
363
376
|
}
|
|
364
|
-
await askToOpenAntigravity(rootPath, appName, options.
|
|
377
|
+
await askToOpenAntigravity(rootPath, appName, options.startAgy);
|
|
365
378
|
}
|
|
@@ -23,6 +23,7 @@ const projectUtils_1 = require("../../projectUtils");
|
|
|
23
23
|
const logger_1 = require("../../logger");
|
|
24
24
|
const iam_1 = require("../iam");
|
|
25
25
|
const error_1 = require("../../error");
|
|
26
|
+
const freeTrial_1 = require("../../dataconnect/freeTrial");
|
|
26
27
|
const API_VERSION = "v1";
|
|
27
28
|
const client = new apiv2_1.Client({
|
|
28
29
|
urlPrefix: (0, api_1.cloudSQLAdminOrigin)(),
|
|
@@ -91,7 +92,7 @@ async function createInstance(args) {
|
|
|
91
92
|
return;
|
|
92
93
|
}
|
|
93
94
|
catch (err) {
|
|
94
|
-
|
|
95
|
+
await handleCreateInstanceError(err, args.location, args.projectId);
|
|
95
96
|
throw err;
|
|
96
97
|
}
|
|
97
98
|
}
|
|
@@ -119,10 +120,14 @@ async function updateInstanceForDataConnect(instance, enableGoogleMlIntegration)
|
|
|
119
120
|
});
|
|
120
121
|
return pollRes;
|
|
121
122
|
}
|
|
122
|
-
function
|
|
123
|
-
if (err
|
|
123
|
+
async function handleCreateInstanceError(err, region, projectId) {
|
|
124
|
+
if (err?.message?.includes("Not allowed to set system label: firebase-data-connect")) {
|
|
124
125
|
throw new error_1.FirebaseError(`Cloud SQL free trial instances are not yet available in ${region}. Please check https://firebase.google.com/docs/data-connect/ for a full list of available regions.`);
|
|
125
126
|
}
|
|
127
|
+
if (err?.message?.includes("The billing account is not in good standing") &&
|
|
128
|
+
(await (0, freeTrial_1.checkFreeTrialInstanceUsed)(projectId))) {
|
|
129
|
+
throw new error_1.FirebaseError(`You have already used your Cloud SQL free trial. To create more instances, you need to attach a billing account to project ${projectId}.`);
|
|
130
|
+
}
|
|
126
131
|
}
|
|
127
132
|
function setDatabaseFlag(flag, flags = []) {
|
|
128
133
|
const temp = flags.filter((f) => f.name !== flag.name);
|
|
@@ -64,8 +64,9 @@ Here are a list of prerequisite steps that must be completed before running a te
|
|
|
64
64
|
|
|
65
65
|
* Goal (required): In one sentence or less, describe what you want the agent to do in this step.
|
|
66
66
|
* Hint (optional): Provide additional information to help Gemini understand and navigate your app.
|
|
67
|
-
* Final Screen Assertion (required for last step): Your final screen assertion should be phrased as an observation, such
|
|
68
|
-
success message' or 'The checkout page is visible'.
|
|
67
|
+
* Final Screen Assertion (required for last step): Your final screen assertion should be phrased as an observation, such
|
|
68
|
+
as 'The screen shows a success message' or 'The checkout page is visible'. You can think of these as test assertions
|
|
69
|
+
that are checked at the end of the step. Optional except for the last step, for which it is required.
|
|
69
70
|
|
|
70
71
|
The developer has optionally specified the following description for their test:
|
|
71
72
|
* ${testDescription}
|
|
@@ -79,8 +80,8 @@ Here are a list of prerequisite steps that must be completed before running a te
|
|
|
79
80
|
For example, if a step has a list in it, it should probably be broken up into multiple steps. Steps do not need
|
|
80
81
|
to be too small though. The test case should provide a good balance between strict guidance and flexibility. As a
|
|
81
82
|
rule of thumb, each step should require between 2-5 actions.
|
|
82
|
-
* Include a hint and
|
|
83
|
-
the agent determine when the goal has been completed.
|
|
83
|
+
* Include a hint and final screen assertion whenever possible. Specifically, try to always include a final screen assertion
|
|
84
|
+
to help the agent determine when the goal has been completed.
|
|
84
85
|
* Avoid functionality that the app testing agent struggles with. The app testing agent struggles with the following:
|
|
85
86
|
* Journeys that require specific timing (like observing that something should be visible for a certain number of
|
|
86
87
|
seconds), interacting with moving or transient elements, etc.
|
|
@@ -96,7 +97,7 @@ Here are a list of prerequisite steps that must be completed before running a te
|
|
|
96
97
|
above, convert the test description provided by the user to make it easier for the agent to follow so that the tests can
|
|
97
98
|
be re-run reliably. If there is no test description, generate a test case that you think will be useful given the functionality
|
|
98
99
|
of the app. Generate an explanation on why you generated the new test case the way you did, and then generate the
|
|
99
|
-
new test case, which again is an array of steps where each step contains a goal, hint, and
|
|
100
|
+
new test case, which again is an array of steps where each step contains a goal, hint, and final screen assertion. Show this
|
|
100
101
|
to the user and have them confirm before moving forward.
|
|
101
102
|
|
|
102
103
|
## Run Test
|
|
@@ -27,7 +27,7 @@ const AiStepSchema = zod_1.z
|
|
|
27
27
|
finalScreenAssertion: zod_1.z
|
|
28
28
|
.string()
|
|
29
29
|
.optional()
|
|
30
|
-
.describe("A description of
|
|
30
|
+
.describe("A description of what the screen should look like at the end of the step, to determine if the goal has been successfully completed."),
|
|
31
31
|
})
|
|
32
32
|
.describe("Step within a test case; run during the execution of the test.");
|
|
33
33
|
const defaultDevices = [
|
package/package.json
CHANGED