kasy-cli 1.5.3 → 1.7.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/bin/kasy.js +7 -4
- package/docs/cli-reference.md +4 -6
- package/lib/commands/add.js +128 -102
- package/lib/commands/check.js +55 -38
- package/lib/commands/codemagic.js +61 -58
- package/lib/commands/deploy.js +49 -45
- package/lib/commands/docs.js +19 -18
- package/lib/commands/doctor.js +46 -44
- package/lib/commands/features.js +42 -20
- package/lib/commands/ios.js +69 -69
- package/lib/commands/new.js +529 -771
- package/lib/commands/notifications.js +59 -59
- package/lib/commands/remove.js +28 -27
- package/lib/commands/run.js +3 -1
- package/lib/commands/update.js +104 -96
- package/lib/commands/validate.js +24 -19
- package/lib/scaffold/catalog.js +45 -11
- package/lib/scaffold/features/README.md +1 -2
- package/lib/scaffold/shared/generator-utils.js +1 -1
- package/lib/utils/apple-release.js +23 -11
- package/lib/utils/brand.js +72 -0
- package/lib/utils/checks.js +20 -9
- package/lib/utils/i18n.js +102 -78
- package/lib/utils/prompts.js +29 -177
- package/lib/utils/ui.js +92 -0
- package/lib/utils/updates.js +9 -8
- package/package.json +2 -1
- package/templates/firebase/lib/features/home/home_page.dart +0 -19
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_routes.dart +0 -1
- package/templates/firebase/lib/router.dart +0 -9
- package/templates/firebase/lib/features/dev/keyboard_test_page.dart +0 -93
package/lib/commands/new.js
CHANGED
|
@@ -14,37 +14,9 @@
|
|
|
14
14
|
const path = require('node:path');
|
|
15
15
|
const crypto = require('node:crypto');
|
|
16
16
|
const kleur = require('kleur');
|
|
17
|
-
const gradient = require('gradient-string');
|
|
18
|
-
const oraPackage = require('ora');
|
|
19
|
-
const ora = oraPackage.default || oraPackage;
|
|
20
17
|
|
|
21
18
|
function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
|
|
22
19
|
|
|
23
|
-
// Spinner manager for multi-step long-running operations.
|
|
24
|
-
// Each call to .next(text) succeeds the previous step and starts a new one.
|
|
25
|
-
// Call .succeed(text) or .fail(text) to close the last step.
|
|
26
|
-
function makeProgressSpinner() {
|
|
27
|
-
let current = null;
|
|
28
|
-
return {
|
|
29
|
-
next(text) {
|
|
30
|
-
if (current) current.succeed();
|
|
31
|
-
current = ora(` ${text}`).start();
|
|
32
|
-
},
|
|
33
|
-
succeed(text) {
|
|
34
|
-
if (current) { current.succeed(text ? ` ${text}` : undefined); current = null; }
|
|
35
|
-
},
|
|
36
|
-
fail(text) {
|
|
37
|
-
if (current) { current.fail(text ? ` ${text}` : undefined); current = null; }
|
|
38
|
-
},
|
|
39
|
-
warn(text) {
|
|
40
|
-
if (current) { current.warn(text ? ` ${text}` : undefined); current = null; }
|
|
41
|
-
},
|
|
42
|
-
stop() {
|
|
43
|
-
if (current) { current.stop(); current = null; }
|
|
44
|
-
},
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
|
|
48
20
|
function generateWebhookKey() {
|
|
49
21
|
return 'rc_wh_' + crypto.randomBytes(16).toString('hex');
|
|
50
22
|
}
|
|
@@ -68,8 +40,8 @@ function openUrl(url) {
|
|
|
68
40
|
exec(cmd, { shell: true });
|
|
69
41
|
} catch (_) {}
|
|
70
42
|
}
|
|
71
|
-
const prompts = require('prompts');
|
|
72
43
|
const ui = require('../utils/ui');
|
|
44
|
+
const { printBanner, infoBox, successBox } = require('../utils/brand');
|
|
73
45
|
const fs = require('fs-extra');
|
|
74
46
|
const { createTranslator } = require('../utils/i18n');
|
|
75
47
|
const { getStoredLanguage, setStoredLanguage } = require('../utils/license');
|
|
@@ -111,51 +83,37 @@ const BILLING_OTHER = '__other__';
|
|
|
111
83
|
async function promptBillingAccountIfNeeded(tr, onCancel) {
|
|
112
84
|
const billingList = await listBillingAccounts();
|
|
113
85
|
if (!billingList.ok) return null;
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
validate: (v) => (v && v.trim() ? true : tr('new.firebase.q.billingAccount.manualId.required')),
|
|
123
|
-
},
|
|
124
|
-
{ onCancel }
|
|
125
|
-
);
|
|
126
|
-
return manualId?.trim() || null;
|
|
127
|
-
}
|
|
128
|
-
const choices = [
|
|
129
|
-
...billingList.accounts.map((a) => ({
|
|
130
|
-
title: `${a.name || a.id} (${a.id})`,
|
|
131
|
-
value: a.id,
|
|
132
|
-
})),
|
|
133
|
-
{ title: tr('new.firebase.q.billingAccount.other'), value: BILLING_OTHER },
|
|
134
|
-
];
|
|
135
|
-
console.log(kleur.yellow(` ${tr('new.firebase.q.billingAccount.context')}`));
|
|
136
|
-
const { billingAccountId } = await prompts(
|
|
137
|
-
{
|
|
138
|
-
type: 'select',
|
|
139
|
-
name: 'billingAccountId',
|
|
140
|
-
message: tr('new.firebase.q.billingAccount'),
|
|
141
|
-
hint: tr('new.firebase.q.billingAccount.hint'),
|
|
142
|
-
choices,
|
|
143
|
-
},
|
|
144
|
-
{ onCancel }
|
|
145
|
-
);
|
|
146
|
-
if (billingAccountId === BILLING_OTHER) {
|
|
147
|
-
const { manualId } = await prompts(
|
|
148
|
-
{
|
|
149
|
-
type: 'text',
|
|
150
|
-
name: 'manualId',
|
|
151
|
-
message: tr('new.firebase.q.billingAccount.manualId'),
|
|
152
|
-
hint: tr('new.firebase.q.billingAccount.manualId.hint'),
|
|
153
|
-
validate: (v) => (v && v.trim() ? true : tr('new.firebase.q.billingAccount.manualId.required')),
|
|
154
|
-
},
|
|
155
|
-
{ onCancel }
|
|
156
|
-
);
|
|
86
|
+
|
|
87
|
+
const askManualId = async () => {
|
|
88
|
+
const manualId = await ui.text({
|
|
89
|
+
message: tr('new.firebase.q.billingAccount.manualId'),
|
|
90
|
+
placeholder: tr('new.firebase.q.billingAccount.manualId.hint'),
|
|
91
|
+
validate: (v) => (v && v.trim() ? undefined : tr('new.firebase.q.billingAccount.manualId.required')),
|
|
92
|
+
onCancel,
|
|
93
|
+
});
|
|
157
94
|
return manualId?.trim() || null;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
if (!billingList.accounts?.length) {
|
|
98
|
+
ui.note(tr('new.firebase.q.billingAccount.context'));
|
|
99
|
+
return askManualId();
|
|
158
100
|
}
|
|
101
|
+
|
|
102
|
+
ui.note(tr('new.firebase.q.billingAccount.context'));
|
|
103
|
+
const billingAccountId = await ui.select({
|
|
104
|
+
message: tr('new.firebase.q.billingAccount'),
|
|
105
|
+
initialValue: billingList.accounts[0].id,
|
|
106
|
+
options: [
|
|
107
|
+
...billingList.accounts.map((a) => ({
|
|
108
|
+
value: a.id,
|
|
109
|
+
label: `${a.name || a.id} (${a.id})`,
|
|
110
|
+
})),
|
|
111
|
+
{ value: BILLING_OTHER, label: tr('new.firebase.q.billingAccount.other') },
|
|
112
|
+
],
|
|
113
|
+
onCancel,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
if (billingAccountId === BILLING_OTHER) return askManualId();
|
|
159
117
|
return billingAccountId;
|
|
160
118
|
}
|
|
161
119
|
|
|
@@ -169,100 +127,77 @@ async function promptBillingAccountIfNeeded(tr, onCancel) {
|
|
|
169
127
|
async function promptOrganizationIfNeeded(tr, onCancel) {
|
|
170
128
|
const orgList = await listGcpOrganizations();
|
|
171
129
|
if (!orgList.ok || !orgList.organizations?.length) return null;
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
hint: tr('new.firebase.q.organization.hint'),
|
|
185
|
-
choices,
|
|
186
|
-
},
|
|
187
|
-
{ onCancel }
|
|
188
|
-
);
|
|
130
|
+
const organizationId = await ui.select({
|
|
131
|
+
message: tr('new.firebase.q.organization'),
|
|
132
|
+
initialValue: orgList.organizations[0].id,
|
|
133
|
+
options: [
|
|
134
|
+
...orgList.organizations.map((o) => ({
|
|
135
|
+
value: o.id,
|
|
136
|
+
label: `${o.name} (${o.id})`,
|
|
137
|
+
})),
|
|
138
|
+
{ value: null, label: tr('new.firebase.q.organization.none') },
|
|
139
|
+
],
|
|
140
|
+
onCancel,
|
|
141
|
+
});
|
|
189
142
|
return organizationId || null;
|
|
190
143
|
}
|
|
191
144
|
|
|
192
145
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
193
146
|
|
|
194
|
-
function printBanner(tr) {
|
|
195
|
-
const bar = kleur.gray('─────────────────────────────────────────────────');
|
|
196
|
-
const logo = [
|
|
197
|
-
' ╦╔═ ╔═╗ ╔═╗ ╦ ╦',
|
|
198
|
-
' ╠╩╗ ╠═╣ ╚═╗ ╚╦╝',
|
|
199
|
-
' ╩ ╩ ╩ ╩ ╚═╝ ╩ ',
|
|
200
|
-
]
|
|
201
|
-
.map((line) => gradient(['#a78bfa', '#60a5fa'])(line))
|
|
202
|
-
.join('\n');
|
|
203
|
-
console.log(`\n${bar}\n`);
|
|
204
|
-
console.log(logo);
|
|
205
|
-
console.log('');
|
|
206
|
-
console.log(` ${kleur.dim(tr('new.subtitle2'))}`);
|
|
207
|
-
console.log(`\n${bar}\n`);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
147
|
function printPrerequisites(tr, backend, firebaseSetupMode = 'existing', checkResults = []) {
|
|
211
148
|
const gcloudOk = checkResults.every(
|
|
212
149
|
(r) => !r.name?.includes('gcloud') || r.ok
|
|
213
150
|
);
|
|
214
|
-
console.log(kleur.bold().yellow(` ${tr('new.prereq.title')}`));
|
|
215
151
|
const firebaseCreate = firebaseSetupMode === 'create';
|
|
152
|
+
const items = [];
|
|
153
|
+
|
|
216
154
|
if (backend === 'firebase') {
|
|
217
155
|
if (firebaseCreate) {
|
|
218
|
-
if (!gcloudOk)
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
console.log(kleur.gray(` ${tr('new.firebase.prereq.create.projectQuota')}`));
|
|
223
|
-
console.log(kleur.cyan(` ${tr('new.firebase.prereq.create.note')}`));
|
|
156
|
+
if (!gcloudOk) items.push(tr('new.firebase.prereq.create.1'));
|
|
157
|
+
items.push(tr('new.firebase.prereq.create.2'));
|
|
158
|
+
items.push(tr('new.firebase.prereq.create.projectQuota'));
|
|
159
|
+
items.push(tr('new.firebase.prereq.create.note'));
|
|
224
160
|
} else {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
161
|
+
items.push(tr('new.firebase.prereq.1'));
|
|
162
|
+
items.push(tr('new.firebase.prereq.2'));
|
|
163
|
+
items.push(tr('new.firebase.prereq.3'));
|
|
164
|
+
items.push(tr('new.firebase.prereq.4'));
|
|
165
|
+
items.push(tr('new.firebase.prereq.5'));
|
|
166
|
+
items.push(tr('new.firebase.prereq.doc'));
|
|
231
167
|
}
|
|
232
168
|
} else if (backend === 'supabase') {
|
|
233
169
|
if (firebaseCreate) {
|
|
234
|
-
if (!gcloudOk)
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
console.log(kleur.gray(` ${tr('new.supabase.prereq.2')}`));
|
|
242
|
-
console.log(kleur.cyan(` ${tr('new.supabase.prereq.login')}`));
|
|
170
|
+
if (!gcloudOk) items.push(tr('new.firebase.prereq.create.1'));
|
|
171
|
+
items.push(tr('new.firebase.prereq.create.2'));
|
|
172
|
+
items.push(tr('new.firebase.prereq.create.projectQuota'));
|
|
173
|
+
items.push(tr('new.firebase.prereq.create.pushNote'));
|
|
174
|
+
items.push(tr('new.supabase.prereq.1'));
|
|
175
|
+
items.push(tr('new.supabase.prereq.2'));
|
|
176
|
+
items.push(tr('new.supabase.prereq.login'));
|
|
243
177
|
} else {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
178
|
+
items.push(tr('new.supabase.prereq.1'));
|
|
179
|
+
items.push(tr('new.supabase.prereq.2'));
|
|
180
|
+
items.push(tr('new.supabase.prereq.3'));
|
|
181
|
+
items.push(tr('new.supabase.prereq.login'));
|
|
248
182
|
}
|
|
249
183
|
} else if (backend === 'api') {
|
|
250
184
|
if (firebaseCreate) {
|
|
251
|
-
if (!gcloudOk)
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
console.log(kleur.gray(` ${tr('new.api.prereq.1')}`));
|
|
258
|
-
console.log(kleur.gray(` ${tr('new.api.prereq.2')}`));
|
|
185
|
+
if (!gcloudOk) items.push(tr('new.firebase.prereq.create.1'));
|
|
186
|
+
items.push(tr('new.firebase.prereq.create.2'));
|
|
187
|
+
items.push(tr('new.firebase.prereq.create.projectQuota'));
|
|
188
|
+
items.push(tr('new.firebase.prereq.create.pushNote'));
|
|
189
|
+
items.push(tr('new.api.prereq.1'));
|
|
190
|
+
items.push(tr('new.api.prereq.2'));
|
|
259
191
|
} else {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
192
|
+
items.push(tr('new.api.prereq.1'));
|
|
193
|
+
items.push(tr('new.api.prereq.2'));
|
|
194
|
+
items.push(tr('new.api.prereq.3'));
|
|
263
195
|
}
|
|
264
196
|
}
|
|
265
|
-
|
|
197
|
+
|
|
198
|
+
if (items.length > 0) {
|
|
199
|
+
ui.note(items.map((i) => `• ${i}`).join('\n'), tr('new.prereq.title'));
|
|
200
|
+
}
|
|
266
201
|
}
|
|
267
202
|
|
|
268
203
|
function printSummary(tr, answers) {
|
|
@@ -271,25 +206,18 @@ function printSummary(tr, answers) {
|
|
|
271
206
|
: tr('new.firebase.confirm.none');
|
|
272
207
|
|
|
273
208
|
const backendLabel = { firebase: '🔥 Firebase', supabase: '🟢 Supabase', api: '🔗 API REST' }[answers.backend] || answers.backend;
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
if (answers.
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
console.log(` ${kleur.dim('Supabase URL:')} ${kleur.white(answers.supabaseUrl)}`);
|
|
287
|
-
}
|
|
288
|
-
if (answers.apiBaseUrl) {
|
|
289
|
-
console.log(` ${kleur.dim('API URL:')} ${kleur.white(answers.apiBaseUrl)}`);
|
|
290
|
-
}
|
|
291
|
-
console.log(` ${kleur.dim(tr('new.firebase.confirm.modules') + ':')} ${kleur.white(modules)}`);
|
|
292
|
-
console.log(bar);
|
|
209
|
+
|
|
210
|
+
const rows = [
|
|
211
|
+
`${kleur.dim(tr('new.firebase.confirm.app') + ':')} ${kleur.white(answers.appName)}`,
|
|
212
|
+
`${kleur.dim('Bundle:')} ${kleur.white(answers.bundleId)}`,
|
|
213
|
+
`${kleur.dim('Backend:')} ${kleur.white(backendLabel)}`,
|
|
214
|
+
];
|
|
215
|
+
if (answers.firebaseProjectId) rows.push(`${kleur.dim('Firebase:')} ${kleur.white(answers.firebaseProjectId)}`);
|
|
216
|
+
if (answers.supabaseUrl) rows.push(`${kleur.dim('Supabase URL:')} ${kleur.white(answers.supabaseUrl)}`);
|
|
217
|
+
if (answers.apiBaseUrl) rows.push(`${kleur.dim('API URL:')} ${kleur.white(answers.apiBaseUrl)}`);
|
|
218
|
+
rows.push(`${kleur.dim(tr('new.firebase.confirm.modules') + ':')} ${kleur.white(modules)}`);
|
|
219
|
+
|
|
220
|
+
console.log(infoBox(`📦 ${tr('new.firebase.confirm.title')}`, rows.join('\n')));
|
|
293
221
|
}
|
|
294
222
|
|
|
295
223
|
const STEP_LABELS = {
|
|
@@ -378,38 +306,35 @@ function stepProgress(key, lang) {
|
|
|
378
306
|
function printCreateFromScratchStatus(result, tr) {
|
|
379
307
|
if (result.sha1Skipped) {
|
|
380
308
|
const err = (result.sha1Error || '').replace(/\s+/g, ' ').slice(0, 120);
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
}
|
|
386
|
-
console.log(kleur.yellow(` ${tr('new.sha1.addManually')}`));
|
|
387
|
-
if (result.sha1ManualUrl) console.log(kleur.cyan(` ${result.sha1ManualUrl}`));
|
|
309
|
+
const msg = result.sha1Skipped === 'api_failed'
|
|
310
|
+
? tr('new.sha1.skipped.apiFailed', { error: err })
|
|
311
|
+
: tr('new.sha1.skipped.other', { reason: result.sha1Skipped });
|
|
312
|
+
const url = result.sha1ManualUrl ? `\n${kleur.cyan(result.sha1ManualUrl)}` : '';
|
|
313
|
+
ui.log.warn(`${msg}\n${tr('new.sha1.addManually')}${url}`);
|
|
388
314
|
} else {
|
|
389
|
-
|
|
315
|
+
ui.log.success(tr('new.sha1.added'));
|
|
390
316
|
}
|
|
391
317
|
|
|
392
318
|
if (result.firestoreCreated) {
|
|
393
|
-
|
|
319
|
+
ui.log.success(tr('new.firestore.created'));
|
|
394
320
|
} else {
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
321
|
+
const msg = result.firestoreError
|
|
322
|
+
? tr('new.firestore.notCreated.error', { error: (result.firestoreError || '').slice(0, 100) })
|
|
323
|
+
: tr('new.firestore.notCreated');
|
|
324
|
+
ui.log.warn(`${msg}\n${tr('new.activateManually')}\n${kleur.cyan(result.firestoreUrl)}`);
|
|
399
325
|
}
|
|
400
326
|
|
|
401
327
|
if (result.storageCreated) {
|
|
402
|
-
|
|
328
|
+
ui.log.success(tr('new.storage.created'));
|
|
403
329
|
} else {
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
330
|
+
const msg = result.storageError
|
|
331
|
+
? tr('new.storage.notCreated.error', { error: (result.storageError || '').slice(0, 100) })
|
|
332
|
+
: tr('new.storage.notCreated');
|
|
333
|
+
ui.log.warn(`${msg}\n${tr('new.activateManually')}\n${kleur.cyan(result.storageUrl)}`);
|
|
408
334
|
}
|
|
409
335
|
}
|
|
410
336
|
|
|
411
337
|
function printSuccessCard(tr, answers, targetDir) {
|
|
412
|
-
const bar = kleur.gray(' ─────────────────────────────────────────────');
|
|
413
338
|
const folderName = path.basename(targetDir);
|
|
414
339
|
const consoleUrl = answers.backend === 'firebase' && answers.firebaseProjectId
|
|
415
340
|
? `https://console.firebase.google.com/project/${answers.firebaseProjectId}`
|
|
@@ -417,71 +342,62 @@ function printSuccessCard(tr, answers, targetDir) {
|
|
|
417
342
|
? 'https://supabase.com/dashboard'
|
|
418
343
|
: answers.apiBaseUrl || null;
|
|
419
344
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
console.log('');
|
|
426
|
-
console.log(` ${kleur.dim('1.')} ${kleur.dim(tr('new.success.step.cd'))}`);
|
|
427
|
-
console.log(` ${kleur.cyan(`cd ${folderName}`)}`);
|
|
428
|
-
console.log('');
|
|
345
|
+
const lines = [];
|
|
346
|
+
lines.push(kleur.bold(tr('new.success.nextSteps')));
|
|
347
|
+
lines.push('');
|
|
348
|
+
lines.push(`${kleur.dim('1.')} ${kleur.dim(tr('new.success.step.cd'))}`);
|
|
349
|
+
lines.push(` ${kleur.cyan(`cd ${folderName}`)}`);
|
|
429
350
|
let stepNum = 2;
|
|
430
351
|
if (answers.backend === 'firebase') {
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
352
|
+
lines.push('');
|
|
353
|
+
lines.push(`${kleur.dim(`${stepNum++}.`)} ${kleur.dim(tr('new.success.step.deploy'))}`);
|
|
354
|
+
lines.push(` ${kleur.cyan('kasy deploy')}`);
|
|
434
355
|
}
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
356
|
+
lines.push('');
|
|
357
|
+
lines.push(`${kleur.dim(`${stepNum++}.`)} ${kleur.dim(tr('new.success.step.run'))}`);
|
|
358
|
+
lines.push(` ${kleur.cyan('kasy run')}`);
|
|
359
|
+
lines.push(` ${kleur.dim(tr('new.success.step.run.vscode'))}`);
|
|
438
360
|
if (consoleUrl) {
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
361
|
+
lines.push('');
|
|
362
|
+
lines.push(`${kleur.dim(`${stepNum}.`)} ${kleur.dim(tr('new.success.step.console'))}`);
|
|
363
|
+
lines.push(` ${kleur.cyan(consoleUrl)}`);
|
|
442
364
|
}
|
|
443
|
-
|
|
444
|
-
console.log(
|
|
445
|
-
console.log('');
|
|
365
|
+
|
|
366
|
+
console.log(successBox(`🎉 ${tr('new.success.title')}`, lines.join('\n')));
|
|
446
367
|
}
|
|
447
368
|
|
|
448
369
|
function printStepResult(step, lang = 'pt') {
|
|
370
|
+
const label = stepLabel(step.name, lang);
|
|
449
371
|
if (step.skipped) {
|
|
450
|
-
|
|
451
|
-
console.log(` ${kleur.gray('⏭')} ${kleur.gray(label)} ${kleur.gray('(skipped)')}`);
|
|
372
|
+
ui.log.step(`${kleur.dim(label)} ${kleur.dim('(skipped)')}`);
|
|
452
373
|
return;
|
|
453
374
|
}
|
|
454
|
-
const
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
375
|
+
const detail = step.detail ? kleur.dim(` — ${step.detail.split('\n')[0]}`) : '';
|
|
376
|
+
if (step.ok) {
|
|
377
|
+
ui.log.success(`${label}${detail}`);
|
|
378
|
+
} else {
|
|
379
|
+
ui.log.error(`${label}${detail}`);
|
|
380
|
+
}
|
|
459
381
|
}
|
|
460
382
|
|
|
461
383
|
function onCancel(tr) {
|
|
462
|
-
|
|
384
|
+
ui.cancel(tr('new.firebase.error.aborted'));
|
|
463
385
|
process.exit(0);
|
|
464
386
|
}
|
|
465
387
|
|
|
466
388
|
async function promptSupabaseManual(tr, cancel) {
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
message: tr('prompt.supabase.anonKey.enter'),
|
|
480
|
-
validate: (v) => (v && v.trim() ? true : tr('prompt.supabase.anonKey.required')),
|
|
481
|
-
},
|
|
482
|
-
],
|
|
483
|
-
{ onCancel: cancel }
|
|
484
|
-
);
|
|
389
|
+
const supabaseUrl = await ui.text({
|
|
390
|
+
message: tr('prompt.supabase.url.enter'),
|
|
391
|
+
placeholder: 'https://xxxx.supabase.co',
|
|
392
|
+
validate: (v) => (v && v.trim() ? undefined : tr('prompt.supabase.url.required')),
|
|
393
|
+
onCancel: cancel,
|
|
394
|
+
});
|
|
395
|
+
const supabaseAnonKey = await ui.text({
|
|
396
|
+
message: tr('prompt.supabase.anonKey.enter'),
|
|
397
|
+
validate: (v) => (v && v.trim() ? undefined : tr('prompt.supabase.anonKey.required')),
|
|
398
|
+
onCancel: cancel,
|
|
399
|
+
});
|
|
400
|
+
return { supabaseUrl, supabaseAnonKey };
|
|
485
401
|
}
|
|
486
402
|
|
|
487
403
|
// ── Module presets ────────────────────────────────────────────────────────────
|
|
@@ -568,7 +484,7 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
568
484
|
});
|
|
569
485
|
} else {
|
|
570
486
|
const backendLabel = { firebase: '🔥 Firebase', supabase: '🟢 Supabase', api: '🔗 API REST' }[backend] || backend;
|
|
571
|
-
|
|
487
|
+
ui.log.info(`Backend: ${kleur.white(backendLabel)}`);
|
|
572
488
|
}
|
|
573
489
|
|
|
574
490
|
// ── 3b. Backend tool checks (required — blocks if missing) ───────────────
|
|
@@ -577,7 +493,7 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
577
493
|
let backendCheckResults = [];
|
|
578
494
|
if (backendChecks.length > 0) {
|
|
579
495
|
if (backend === 'supabase' || backend === 'api') {
|
|
580
|
-
|
|
496
|
+
ui.log.info(tr('new.checks.firebaseForPush'));
|
|
581
497
|
}
|
|
582
498
|
const backendLabel = tr('setup.checks.backend', { backend }) || ` Checking ${backend} tools…`;
|
|
583
499
|
backendCheckResults = await runChecks(backendChecks, backendLabel, {
|
|
@@ -587,14 +503,15 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
587
503
|
doneLabel: tr('setup.checks.backend.done', { backend }) || `${backend} tools ready`,
|
|
588
504
|
});
|
|
589
505
|
if (hasRequiredFailures(backendCheckResults)) {
|
|
590
|
-
|
|
591
|
-
console.log(kleur.dim(`\n ${tr('new.checks.installFirebase')}`));
|
|
506
|
+
const installSteps = [tr('new.checks.installFirebase')];
|
|
592
507
|
if (backend === 'supabase') {
|
|
593
508
|
const platform = process.platform === 'darwin' ? 'darwin' : process.platform === 'win32' ? 'win32' : 'linux';
|
|
594
509
|
const supabaseKey = `new.checks.installSupabase.${platform}`;
|
|
595
|
-
|
|
510
|
+
installSteps.push(tr(supabaseKey) || tr('new.checks.installSupabase'));
|
|
596
511
|
}
|
|
597
|
-
|
|
512
|
+
ui.log.error(tr('new.checks.requiredBlock'));
|
|
513
|
+
ui.note(installSteps.map((s) => `• ${s}`).join('\n'));
|
|
514
|
+
ui.cancel(tr('new.firebase.error.aborted'));
|
|
598
515
|
process.exit(1);
|
|
599
516
|
}
|
|
600
517
|
}
|
|
@@ -603,7 +520,8 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
603
520
|
let core;
|
|
604
521
|
if (yes) {
|
|
605
522
|
if (!hasExplicitDir) {
|
|
606
|
-
|
|
523
|
+
ui.log.error('--yes requires an app name: kasy new MyApp --yes');
|
|
524
|
+
ui.cancel(tr('new.firebase.error.aborted'));
|
|
607
525
|
process.exit(1);
|
|
608
526
|
}
|
|
609
527
|
const appName = path.basename(targetDir);
|
|
@@ -614,8 +532,8 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
614
532
|
.replace(/[^a-z0-9]/g, '');
|
|
615
533
|
const bundleId = (slug && !/^\d/.test(slug)) ? `com.${slug}.app` : 'com.example.app';
|
|
616
534
|
core = { appName, bundleId };
|
|
617
|
-
|
|
618
|
-
|
|
535
|
+
ui.log.info(`App: ${kleur.white(appName)}`);
|
|
536
|
+
ui.log.info(`Bundle: ${kleur.white(bundleId)}`);
|
|
619
537
|
} else {
|
|
620
538
|
const appName = await ui.text({
|
|
621
539
|
message: tr('new.firebase.q.appName'),
|
|
@@ -735,96 +653,70 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
735
653
|
} else {
|
|
736
654
|
let firebaseProjectId = projectHint?.trim() || '';
|
|
737
655
|
if (!firebaseProjectId && backend === 'firebase') {
|
|
738
|
-
const
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
validate: (v) => (v && v.trim() ? true : tr('new.firebase.q.projectId.required')),
|
|
745
|
-
},
|
|
746
|
-
{ onCancel: cancel }
|
|
747
|
-
);
|
|
656
|
+
const pid = await ui.text({
|
|
657
|
+
message: tr('new.firebase.q.projectId'),
|
|
658
|
+
placeholder: tr('new.firebase.q.projectId.hint'),
|
|
659
|
+
validate: (v) => (v && v.trim() ? undefined : tr('new.firebase.q.projectId.required')),
|
|
660
|
+
onCancel: cancel,
|
|
661
|
+
});
|
|
748
662
|
firebaseProjectId = pid?.trim() || '';
|
|
749
663
|
}
|
|
750
664
|
if (firebaseProjectId) {
|
|
751
665
|
core.firebaseProjectId = firebaseProjectId;
|
|
752
|
-
|
|
666
|
+
ui.log.info(`Project: ${kleur.white(firebaseProjectId)}`);
|
|
753
667
|
}
|
|
754
668
|
}
|
|
755
669
|
|
|
756
670
|
// ── Firebase region — Quick mode uses default (us-central1) ──────────
|
|
757
671
|
let firebaseRegion = 'us-central1';
|
|
758
672
|
if (backend === 'firebase' && !isQuick) {
|
|
759
|
-
const
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
initial: 0,
|
|
770
|
-
},
|
|
771
|
-
{ onCancel: cancel }
|
|
772
|
-
);
|
|
673
|
+
const region = await ui.select({
|
|
674
|
+
message: tr('new.firebase.q.region'),
|
|
675
|
+
initialValue: 'us-central1',
|
|
676
|
+
options: [
|
|
677
|
+
{ value: 'us-central1', label: tr('new.firebase.q.region.us') },
|
|
678
|
+
{ value: 'europe-west1', label: tr('new.firebase.q.region.europe') },
|
|
679
|
+
{ value: 'southamerica-east1', label: tr('new.firebase.q.region.brazil') },
|
|
680
|
+
],
|
|
681
|
+
onCancel: cancel,
|
|
682
|
+
});
|
|
773
683
|
firebaseRegion = region || 'us-central1';
|
|
774
684
|
}
|
|
775
685
|
|
|
776
686
|
// ── Firebase: create from scratch (when selected) ─────────────────────────
|
|
777
687
|
let firebaseIncludeWeb = true;
|
|
778
688
|
if (backend === 'firebase' && firebaseSetupMode === 'create') {
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
initial: true,
|
|
785
|
-
},
|
|
786
|
-
{ onCancel: cancel }
|
|
787
|
-
);
|
|
788
|
-
firebaseIncludeWeb = includeWeb !== false;
|
|
689
|
+
firebaseIncludeWeb = await ui.confirm({
|
|
690
|
+
message: tr('new.firebase.create.includeWeb'),
|
|
691
|
+
initialValue: true,
|
|
692
|
+
onCancel: cancel,
|
|
693
|
+
});
|
|
789
694
|
const gcloudCheck = await checkGcloudAuth();
|
|
790
695
|
if (!gcloudCheck.ok) {
|
|
791
|
-
|
|
696
|
+
ui.log.error(tr('new.firebase.create.gcloudRequired'));
|
|
792
697
|
if (gcloudCheck.missing === 'gcloud') {
|
|
793
698
|
const instructions = getGcloudInstallInstructions();
|
|
794
|
-
|
|
795
|
-
if (instructions.install) {
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
console.log(kleur.gray(` ${instructions.hint}`));
|
|
801
|
-
}
|
|
802
|
-
console.log(kleur.white(` ${tr('new.firebase.create.installAfter')}:`));
|
|
803
|
-
console.log(kleur.cyan(` ${instructions.after}`));
|
|
804
|
-
console.log(kleur.gray(` ${tr('new.firebase.create.installUrl')}: ${instructions.url}`));
|
|
805
|
-
console.log('');
|
|
699
|
+
const noteLines = [tr('new.firebase.create.installTitle')];
|
|
700
|
+
if (instructions.install) noteLines.push(`${tr('new.firebase.create.installCommand')}:\n ${kleur.cyan(instructions.install)}`);
|
|
701
|
+
if (instructions.hint) noteLines.push(instructions.hint);
|
|
702
|
+
noteLines.push(`${tr('new.firebase.create.installAfter')}:\n ${kleur.cyan(instructions.after)}`);
|
|
703
|
+
noteLines.push(`${tr('new.firebase.create.installUrl')}: ${instructions.url}`);
|
|
704
|
+
ui.note(noteLines.join('\n\n'));
|
|
806
705
|
} else {
|
|
807
|
-
|
|
808
|
-
console.log('');
|
|
706
|
+
ui.note(tr('new.firebase.create.authCommand'));
|
|
809
707
|
}
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
validate: (v) => (v && v.trim() ? true : tr('new.firebase.q.projectId.required')),
|
|
818
|
-
},
|
|
819
|
-
{ onCancel: cancel }
|
|
820
|
-
);
|
|
821
|
-
core.firebaseProjectId = fallback.firebaseProjectId;
|
|
708
|
+
ui.log.warn(tr('new.firebase.create.fallbackHint'));
|
|
709
|
+
core.firebaseProjectId = await ui.text({
|
|
710
|
+
message: tr('new.firebase.q.projectId'),
|
|
711
|
+
placeholder: tr('new.firebase.q.projectId.hint'),
|
|
712
|
+
validate: (v) => (v && v.trim() ? undefined : tr('new.firebase.q.projectId.required')),
|
|
713
|
+
onCancel: cancel,
|
|
714
|
+
});
|
|
822
715
|
} else {
|
|
823
716
|
const selectedOrgId = await promptOrganizationIfNeeded(tr, cancel);
|
|
824
717
|
let selectedBillingId = await promptBillingAccountIfNeeded(tr, cancel);
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
const ps1 = makeProgressSpinner();
|
|
718
|
+
ui.log.info(`${tr('new.firebase.create.estimatedTime')}\n${tr('new.internet.warning')}`);
|
|
719
|
+
const ps1 = ui.makeStepper();
|
|
828
720
|
ps1.next(tr('new.firebase.create.creating'));
|
|
829
721
|
const setupResult = await setupFromScratch(core.appName.trim(), core.bundleId.trim(), {
|
|
830
722
|
includeWeb: firebaseIncludeWeb,
|
|
@@ -841,15 +733,31 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
841
733
|
ps1.next(stepProgress('storage', language));
|
|
842
734
|
} else if (key === 'auth-providers-warn') {
|
|
843
735
|
ps1.stop();
|
|
844
|
-
|
|
845
|
-
console.log(kleur.cyan(` ${data?.url || ''}`));
|
|
736
|
+
ui.log.warn(`${tr('new.firebase.interactive.authWarn')}\n${kleur.cyan(data?.url || '')}`);
|
|
846
737
|
}
|
|
847
738
|
},
|
|
848
739
|
});
|
|
740
|
+
const askReady = async (readyKey) => {
|
|
741
|
+
const ok = await ui.confirm({
|
|
742
|
+
message: tr(readyKey),
|
|
743
|
+
initialValue: true,
|
|
744
|
+
onCancel: cancel,
|
|
745
|
+
});
|
|
746
|
+
if (!ok) {
|
|
747
|
+
ui.cancel(tr('prompt.cancelled'));
|
|
748
|
+
process.exit(0);
|
|
749
|
+
}
|
|
750
|
+
};
|
|
751
|
+
const showBeforeContinue = (step1Key, authUrl) => {
|
|
752
|
+
ui.note(
|
|
753
|
+
`${tr(step1Key)}\n${kleur.cyan(authUrl)}`,
|
|
754
|
+
tr('new.firebase.create.beforeContinue.title')
|
|
755
|
+
);
|
|
756
|
+
};
|
|
849
757
|
if (setupResult.ok) {
|
|
850
758
|
ps1.succeed(tr('new.firebase.create.success'));
|
|
851
759
|
core.firebaseProjectId = setupResult.projectId;
|
|
852
|
-
|
|
760
|
+
ui.log.info(`Project ID: ${core.firebaseProjectId}`);
|
|
853
761
|
printCreateFromScratchStatus(setupResult, tr);
|
|
854
762
|
const authUrl = `https://console.firebase.google.com/project/${core.firebaseProjectId}/authentication/providers`;
|
|
855
763
|
if (setupResult.authEnabled && !setupResult.googleSignInSkipped) {
|
|
@@ -862,50 +770,33 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
862
770
|
const readyKey = setupResult.googleSignInSkipped
|
|
863
771
|
? 'new.firebase.create.beforeContinue.ready'
|
|
864
772
|
: 'new.firebase.create.beforeContinue.ready.noAuth';
|
|
865
|
-
|
|
866
|
-
console.log(kleur.gray(` ${tr(step1Key)}`));
|
|
867
|
-
console.log(kleur.cyan(` ${authUrl}`));
|
|
773
|
+
showBeforeContinue(step1Key, authUrl);
|
|
868
774
|
openUrl(authUrl);
|
|
869
|
-
|
|
870
|
-
{
|
|
871
|
-
type: 'confirm',
|
|
872
|
-
name: 'ready',
|
|
873
|
-
message: tr(readyKey),
|
|
874
|
-
initial: true,
|
|
875
|
-
},
|
|
876
|
-
{ onCancel: cancel }
|
|
877
|
-
);
|
|
878
|
-
if (!ready) {
|
|
879
|
-
console.log(kleur.gray(` ${tr('prompt.cancelled')}`));
|
|
880
|
-
process.exit(0);
|
|
881
|
-
}
|
|
775
|
+
await askReady(readyKey);
|
|
882
776
|
}
|
|
883
777
|
|
|
884
778
|
} else {
|
|
885
779
|
ps1.fail(tr('new.firebase.create.failed'));
|
|
886
780
|
let lastResult = setupResult;
|
|
887
781
|
while (lastResult.billingFailed && lastResult.projectId) {
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
const { retry } = await prompts(
|
|
893
|
-
{
|
|
894
|
-
type: 'confirm',
|
|
895
|
-
name: 'retry',
|
|
896
|
-
message: tr('new.firebase.create.billingRetry.ready'),
|
|
897
|
-
initial: true,
|
|
898
|
-
},
|
|
899
|
-
{ onCancel: cancel }
|
|
782
|
+
ui.log.error(`${tr('new.firebase.create.failed')}: ${lastResult.error}`);
|
|
783
|
+
ui.note(
|
|
784
|
+
`${kleur.cyan(lastResult.billingManualLink)}\n\n${tr('new.firebase.create.billingRetry.hint')}`,
|
|
785
|
+
tr('new.firebase.create.billingRetry.title')
|
|
900
786
|
);
|
|
787
|
+
const retry = await ui.confirm({
|
|
788
|
+
message: tr('new.firebase.create.billingRetry.ready'),
|
|
789
|
+
initialValue: true,
|
|
790
|
+
onCancel: cancel,
|
|
791
|
+
});
|
|
901
792
|
if (!retry) {
|
|
902
|
-
|
|
903
|
-
|
|
793
|
+
ui.log.message(tr('new.firebase.create.billingRetry.exit'));
|
|
794
|
+
ui.log.info(tr('new.firebase.create.useExistingHint', { id: lastResult.projectId }));
|
|
904
795
|
process.exit(0);
|
|
905
796
|
}
|
|
906
|
-
|
|
797
|
+
ui.log.message(tr('new.firebase.create.billingRetry.retrying'));
|
|
907
798
|
selectedBillingId = await promptBillingAccountIfNeeded(tr, cancel);
|
|
908
|
-
const ps2 =
|
|
799
|
+
const ps2 = ui.makeStepper();
|
|
909
800
|
ps2.next(tr('new.firebase.create.creating'));
|
|
910
801
|
lastResult = await setupFromScratch(core.appName.trim(), core.bundleId.trim(), {
|
|
911
802
|
includeWeb: firebaseIncludeWeb,
|
|
@@ -927,7 +818,7 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
927
818
|
if (lastResult.ok) {
|
|
928
819
|
ps2.succeed(tr('new.firebase.create.success'));
|
|
929
820
|
core.firebaseProjectId = lastResult.projectId;
|
|
930
|
-
|
|
821
|
+
ui.log.info(`Project ID: ${core.firebaseProjectId}`);
|
|
931
822
|
printCreateFromScratchStatus(lastResult, tr);
|
|
932
823
|
const authUrl = `https://console.firebase.google.com/project/${core.firebaseProjectId}/authentication/providers`;
|
|
933
824
|
if (lastResult.authEnabled && !lastResult.googleSignInSkipped) {
|
|
@@ -939,47 +830,32 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
939
830
|
const lastReadyKey = lastResult.googleSignInSkipped
|
|
940
831
|
? 'new.firebase.create.beforeContinue.ready'
|
|
941
832
|
: 'new.firebase.create.beforeContinue.ready.noAuth';
|
|
942
|
-
|
|
943
|
-
console.log(kleur.gray(` ${tr(step1Key)}`));
|
|
944
|
-
console.log(kleur.cyan(` ${authUrl}`));
|
|
833
|
+
showBeforeContinue(step1Key, authUrl);
|
|
945
834
|
openUrl(authUrl);
|
|
946
|
-
|
|
947
|
-
{
|
|
948
|
-
type: 'confirm',
|
|
949
|
-
name: 'ready',
|
|
950
|
-
message: tr(lastReadyKey),
|
|
951
|
-
initial: true,
|
|
952
|
-
},
|
|
953
|
-
{ onCancel: cancel }
|
|
954
|
-
);
|
|
955
|
-
if (!ready) {
|
|
956
|
-
console.log(kleur.gray(` ${tr('prompt.cancelled')}`));
|
|
957
|
-
process.exit(0);
|
|
958
|
-
}
|
|
835
|
+
await askReady(lastReadyKey);
|
|
959
836
|
}
|
|
960
837
|
|
|
961
838
|
break;
|
|
839
|
+
} else {
|
|
840
|
+
// Loop will replace ps2 next iteration; close the failed step now
|
|
841
|
+
// so the previous spinner doesn't stay in the running state visually.
|
|
842
|
+
ps2.fail(tr('new.firebase.create.failed'));
|
|
962
843
|
}
|
|
963
844
|
}
|
|
964
845
|
if (!lastResult.ok && !lastResult.billingFailed) {
|
|
965
846
|
const isProjectQuota = /exceeded your allotted project quota|project quota/i.test(String(lastResult.error || ''));
|
|
966
847
|
const msg = isProjectQuota ? tr('new.firebase.create.projectQuotaExceeded') : `${tr('new.firebase.create.failed')}: ${lastResult.error}`;
|
|
967
|
-
|
|
848
|
+
ui.log.error(msg);
|
|
968
849
|
if (lastResult.projectId) {
|
|
969
850
|
core.firebaseProjectId = lastResult.projectId;
|
|
970
|
-
|
|
851
|
+
ui.log.message(tr('new.firebase.create.usingProjectId', { id: lastResult.projectId }));
|
|
971
852
|
} else {
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
validate: (v) => (v && v.trim() ? true : tr('new.firebase.q.projectId.required')),
|
|
979
|
-
},
|
|
980
|
-
{ onCancel: cancel }
|
|
981
|
-
);
|
|
982
|
-
core.firebaseProjectId = fallback.firebaseProjectId;
|
|
853
|
+
core.firebaseProjectId = await ui.text({
|
|
854
|
+
message: tr('new.firebase.q.projectId'),
|
|
855
|
+
placeholder: tr('new.firebase.q.projectId.hint'),
|
|
856
|
+
validate: (v) => (v && v.trim() ? undefined : tr('new.firebase.q.projectId.required')),
|
|
857
|
+
onCancel: cancel,
|
|
858
|
+
});
|
|
983
859
|
}
|
|
984
860
|
}
|
|
985
861
|
}
|
|
@@ -990,42 +866,30 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
990
866
|
if ((backend === 'supabase' || backend === 'api') && firebaseSetupMode === 'create') {
|
|
991
867
|
const gcloudCheck = await checkGcloudAuth();
|
|
992
868
|
if (!gcloudCheck.ok) {
|
|
993
|
-
|
|
869
|
+
ui.log.error(tr('new.firebase.create.gcloudRequired'));
|
|
994
870
|
if (gcloudCheck.missing === 'gcloud') {
|
|
995
871
|
const instructions = getGcloudInstallInstructions();
|
|
996
|
-
|
|
997
|
-
if (instructions.install) {
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
}
|
|
1001
|
-
|
|
1002
|
-
console.log(kleur.gray(` ${instructions.hint}`));
|
|
1003
|
-
}
|
|
1004
|
-
console.log(kleur.white(` ${tr('new.firebase.create.installAfter')}:`));
|
|
1005
|
-
console.log(kleur.cyan(` ${instructions.after}`));
|
|
1006
|
-
console.log(kleur.gray(` ${tr('new.firebase.create.installUrl')}: ${instructions.url}`));
|
|
1007
|
-
console.log('');
|
|
872
|
+
const noteLines = [tr('new.firebase.create.installTitle')];
|
|
873
|
+
if (instructions.install) noteLines.push(`${tr('new.firebase.create.installCommand')}:\n ${kleur.cyan(instructions.install)}`);
|
|
874
|
+
if (instructions.hint) noteLines.push(instructions.hint);
|
|
875
|
+
noteLines.push(`${tr('new.firebase.create.installAfter')}:\n ${kleur.cyan(instructions.after)}`);
|
|
876
|
+
noteLines.push(`${tr('new.firebase.create.installUrl')}: ${instructions.url}`);
|
|
877
|
+
ui.note(noteLines.join('\n\n'));
|
|
1008
878
|
} else {
|
|
1009
|
-
|
|
1010
|
-
console.log('');
|
|
879
|
+
ui.note(tr('new.firebase.create.authCommand'));
|
|
1011
880
|
}
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
validate: (v) => (v && v.trim() ? true : tr('new.firebase.q.projectId.required')),
|
|
1020
|
-
},
|
|
1021
|
-
{ onCancel: cancel }
|
|
1022
|
-
);
|
|
1023
|
-
core.firebaseProjectId = fallback.firebaseProjectId;
|
|
881
|
+
ui.log.warn(tr('new.firebase.create.fallbackHint'));
|
|
882
|
+
core.firebaseProjectId = await ui.text({
|
|
883
|
+
message: tr('new.firebase.q.projectId'),
|
|
884
|
+
placeholder: tr('new.firebase.q.projectId.hint') + ' (FCM + Remote Config)',
|
|
885
|
+
validate: (v) => (v && v.trim() ? undefined : tr('new.firebase.q.projectId.required')),
|
|
886
|
+
onCancel: cancel,
|
|
887
|
+
});
|
|
1024
888
|
} else {
|
|
1025
889
|
const selectedOrgId = await promptOrganizationIfNeeded(tr, cancel);
|
|
1026
890
|
const selectedBillingId = await promptBillingAccountIfNeeded(tr, cancel);
|
|
1027
|
-
|
|
1028
|
-
const ps3 =
|
|
891
|
+
ui.log.info(tr('new.internet.warning'));
|
|
892
|
+
const ps3 = ui.makeStepper();
|
|
1029
893
|
ps3.next(tr('new.firebase.create.creatingPush'));
|
|
1030
894
|
const setupResult = await setupFromScratch(core.appName.trim(), core.bundleId.trim(), {
|
|
1031
895
|
includeWeb: true,
|
|
@@ -1046,26 +910,21 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1046
910
|
if (setupResult.ok) {
|
|
1047
911
|
ps3.succeed(tr('new.firebase.create.successPush'));
|
|
1048
912
|
core.firebaseProjectId = setupResult.projectId;
|
|
1049
|
-
|
|
913
|
+
ui.log.info(`Project ID: ${core.firebaseProjectId}`);
|
|
1050
914
|
printCreateFromScratchStatus(setupResult, tr);
|
|
1051
915
|
} else {
|
|
1052
916
|
ps3.fail(tr('new.firebase.create.failed'));
|
|
1053
|
-
|
|
917
|
+
ui.log.error(`${tr('new.firebase.create.failed')}: ${setupResult.error}`);
|
|
1054
918
|
if (setupResult.projectId) {
|
|
1055
919
|
core.firebaseProjectId = setupResult.projectId;
|
|
1056
|
-
|
|
920
|
+
ui.log.message(tr('new.firebase.create.usingProjectId', { id: setupResult.projectId }));
|
|
1057
921
|
} else {
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
validate: (v) => (v && v.trim() ? true : tr('new.firebase.q.projectId.required')),
|
|
1065
|
-
},
|
|
1066
|
-
{ onCancel: cancel }
|
|
1067
|
-
);
|
|
1068
|
-
core.firebaseProjectId = fallback.firebaseProjectId;
|
|
922
|
+
core.firebaseProjectId = await ui.text({
|
|
923
|
+
message: tr('new.firebase.q.projectId'),
|
|
924
|
+
placeholder: tr('new.firebase.q.projectId.hint') + ' (FCM + Remote Config)',
|
|
925
|
+
validate: (v) => (v && v.trim() ? undefined : tr('new.firebase.q.projectId.required')),
|
|
926
|
+
onCancel: cancel,
|
|
927
|
+
});
|
|
1069
928
|
}
|
|
1070
929
|
}
|
|
1071
930
|
}
|
|
@@ -1079,91 +938,75 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1079
938
|
let supabaseExistingResult = null;
|
|
1080
939
|
|
|
1081
940
|
if (backend === 'supabase') {
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
{
|
|
1094
|
-
|
|
1095
|
-
supabaseCreate = createSupabase;
|
|
941
|
+
supabaseCreate = await ui.select({
|
|
942
|
+
message: tr('new.supabase.q.create'),
|
|
943
|
+
initialValue: true,
|
|
944
|
+
options: [
|
|
945
|
+
{ value: true, label: tr('new.supabase.q.create.create') },
|
|
946
|
+
{ value: false, label: tr('new.supabase.q.create.existing') },
|
|
947
|
+
],
|
|
948
|
+
onCancel: cancel,
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
const showLoginRequired = () => {
|
|
952
|
+
ui.log.warn(`${tr('new.supabase.loginRequired')}\n${kleur.cyan(tr('new.supabase.loginCommand'))}`);
|
|
953
|
+
};
|
|
1096
954
|
|
|
1097
955
|
if (supabaseCreate) {
|
|
1098
956
|
const loginCheck = await checkLoggedIn();
|
|
1099
|
-
if (!loginCheck.ok)
|
|
1100
|
-
console.log(kleur.yellow(` ${tr('new.supabase.loginRequired')}`));
|
|
1101
|
-
console.log(kleur.cyan(` ${tr('new.supabase.loginCommand')}`));
|
|
1102
|
-
console.log('');
|
|
1103
|
-
}
|
|
957
|
+
if (!loginCheck.ok) showLoginRequired();
|
|
1104
958
|
const orgsResult = await getOrgsList();
|
|
1105
959
|
if (!orgsResult.ok || !orgsResult.orgs?.length) {
|
|
1106
|
-
|
|
960
|
+
ui.log.error(tr('new.supabase.orgsRequired'));
|
|
1107
961
|
supabaseCreateResult = { ok: false, error: tr('new.supabase.orgsRequired') };
|
|
1108
962
|
} else {
|
|
1109
963
|
let orgId = orgsResult.orgs[0].id;
|
|
1110
964
|
if (orgsResult.orgs.length > 1) {
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
},
|
|
1118
|
-
{ onCancel: cancel }
|
|
1119
|
-
);
|
|
1120
|
-
orgId = selectedOrgId;
|
|
965
|
+
orgId = await ui.select({
|
|
966
|
+
message: tr('new.supabase.q.orgSelect'),
|
|
967
|
+
initialValue: orgsResult.orgs[0].id,
|
|
968
|
+
options: orgsResult.orgs.map((o) => ({ value: o.id, label: o.name })),
|
|
969
|
+
onCancel: cancel,
|
|
970
|
+
});
|
|
1121
971
|
}
|
|
1122
972
|
let supabaseRegion = DEFAULT_SUPABASE_REGION;
|
|
1123
973
|
if (!isQuick) {
|
|
1124
|
-
const
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
],
|
|
1135
|
-
initial: 0,
|
|
1136
|
-
},
|
|
1137
|
-
{ onCancel: cancel }
|
|
1138
|
-
);
|
|
974
|
+
const region = await ui.select({
|
|
975
|
+
message: tr('new.supabase.q.region'),
|
|
976
|
+
initialValue: 'sa-east-1',
|
|
977
|
+
options: [
|
|
978
|
+
{ value: 'sa-east-1', label: tr('new.supabase.q.region.brazil') },
|
|
979
|
+
{ value: 'us-east-1', label: tr('new.supabase.q.region.us') },
|
|
980
|
+
{ value: 'eu-west-1', label: tr('new.supabase.q.region.europe') },
|
|
981
|
+
],
|
|
982
|
+
onCancel: cancel,
|
|
983
|
+
});
|
|
1139
984
|
supabaseRegion = region || DEFAULT_SUPABASE_REGION;
|
|
1140
985
|
}
|
|
1141
|
-
const
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
validate: (v) => (v && v.length >= 6 ? true : tr('new.supabase.q.dbPassword.required')),
|
|
1147
|
-
},
|
|
1148
|
-
{ onCancel: cancel }
|
|
1149
|
-
);
|
|
986
|
+
const dbPassword = await ui.password({
|
|
987
|
+
message: tr('new.supabase.q.dbPassword'),
|
|
988
|
+
validate: (v) => (v && v.length >= 6 ? undefined : tr('new.supabase.q.dbPassword.required')),
|
|
989
|
+
onCancel: cancel,
|
|
990
|
+
});
|
|
1150
991
|
supabaseDbPassword = dbPassword;
|
|
1151
|
-
|
|
1152
|
-
|
|
992
|
+
ui.log.info(tr('new.internet.warning'));
|
|
993
|
+
const createSpinner = ui.spinner();
|
|
994
|
+
createSpinner.start(tr('new.supabase.creating'));
|
|
1153
995
|
supabaseCreateResult = await createProjectAndGetKeys(
|
|
1154
996
|
core.appName.trim().replace(/\s+/g, '-').toLowerCase(),
|
|
1155
997
|
supabaseDbPassword,
|
|
1156
998
|
supabaseRegion,
|
|
1157
999
|
orgId
|
|
1158
1000
|
);
|
|
1001
|
+
createSpinner.stop(tr('new.supabase.creating'));
|
|
1159
1002
|
}
|
|
1160
1003
|
if (supabaseCreateResult.ok) {
|
|
1161
1004
|
core.supabaseUrl = supabaseCreateResult.supabaseUrl;
|
|
1162
1005
|
core.supabaseAnonKey = supabaseCreateResult.supabaseAnonKey;
|
|
1163
|
-
|
|
1006
|
+
ui.log.success(tr('new.supabase.created'));
|
|
1164
1007
|
} else {
|
|
1165
|
-
|
|
1166
|
-
|
|
1008
|
+
ui.log.error(`${tr('new.supabase.createFailed')}: ${supabaseCreateResult.error}`);
|
|
1009
|
+
ui.log.message(tr('new.supabase.loginHint'));
|
|
1167
1010
|
Object.assign(core, await promptSupabaseManual(tr, cancel));
|
|
1168
1011
|
supabaseCreate = false;
|
|
1169
1012
|
}
|
|
@@ -1171,54 +1014,46 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1171
1014
|
// Usar projeto Supabase existente: org → projeto → keys → senha (mesmo fluxo de setup, sem criar)
|
|
1172
1015
|
const loginCheck = await checkLoggedIn();
|
|
1173
1016
|
if (!loginCheck.ok) {
|
|
1174
|
-
|
|
1175
|
-
console.log(kleur.cyan(` ${tr('new.supabase.loginCommand')}`));
|
|
1176
|
-
console.log('');
|
|
1017
|
+
showLoginRequired();
|
|
1177
1018
|
Object.assign(core, await promptSupabaseManual(tr, cancel));
|
|
1178
1019
|
} else {
|
|
1179
1020
|
const orgsResult = await getOrgsList();
|
|
1180
1021
|
if (!orgsResult.ok || !orgsResult.orgs?.length) {
|
|
1181
|
-
|
|
1022
|
+
ui.log.error(tr('new.supabase.orgsRequired'));
|
|
1182
1023
|
Object.assign(core, await promptSupabaseManual(tr, cancel));
|
|
1183
1024
|
} else {
|
|
1184
1025
|
let orgId = orgsResult.orgs[0].id;
|
|
1185
1026
|
if (orgsResult.orgs.length > 1) {
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1027
|
+
orgId = await ui.select({
|
|
1028
|
+
message: tr('new.supabase.q.useExisting.orgSelect'),
|
|
1029
|
+
initialValue: orgsResult.orgs[0].id,
|
|
1030
|
+
options: orgsResult.orgs.map((o) => ({ value: o.id, label: o.name })),
|
|
1031
|
+
onCancel: cancel,
|
|
1032
|
+
});
|
|
1191
1033
|
}
|
|
1192
1034
|
const projectsResult = await getProjectsByOrg(orgId);
|
|
1193
1035
|
if (!projectsResult.ok || !projectsResult.projects?.length) {
|
|
1194
|
-
|
|
1036
|
+
ui.log.warn(tr('new.supabase.projectsRequired'));
|
|
1195
1037
|
Object.assign(core, await promptSupabaseManual(tr, cancel));
|
|
1196
1038
|
} else {
|
|
1197
|
-
const
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
},
|
|
1204
|
-
{ onCancel: cancel }
|
|
1205
|
-
);
|
|
1039
|
+
const selectedProjectRef = await ui.select({
|
|
1040
|
+
message: tr('new.supabase.q.useExisting.projectSelect'),
|
|
1041
|
+
initialValue: projectsResult.projects[0].id,
|
|
1042
|
+
options: projectsResult.projects.map((p) => ({ value: p.id, label: `${p.name} (${p.id})` })),
|
|
1043
|
+
onCancel: cancel,
|
|
1044
|
+
});
|
|
1206
1045
|
const keysResult = await getProjectKeys(selectedProjectRef);
|
|
1207
1046
|
if (!keysResult.ok) {
|
|
1208
|
-
|
|
1047
|
+
ui.log.error(keysResult.error);
|
|
1209
1048
|
Object.assign(core, await promptSupabaseManual(tr, cancel));
|
|
1210
1049
|
} else {
|
|
1211
1050
|
core.supabaseUrl = keysResult.supabaseUrl;
|
|
1212
1051
|
core.supabaseAnonKey = keysResult.supabaseAnonKey;
|
|
1213
|
-
const
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
validate: (v) => (v && v.length >= 6 ? true : tr('new.supabase.q.dbPassword.required')),
|
|
1219
|
-
},
|
|
1220
|
-
{ onCancel: cancel }
|
|
1221
|
-
);
|
|
1052
|
+
const dbPassword = await ui.password({
|
|
1053
|
+
message: tr('new.supabase.q.dbPassword.existing'),
|
|
1054
|
+
validate: (v) => (v && v.length >= 6 ? undefined : tr('new.supabase.q.dbPassword.required')),
|
|
1055
|
+
onCancel: cancel,
|
|
1056
|
+
});
|
|
1222
1057
|
supabaseExistingResult = {
|
|
1223
1058
|
ok: true,
|
|
1224
1059
|
projectRef: selectedProjectRef,
|
|
@@ -1226,7 +1061,7 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1226
1061
|
supabaseAnonKey: keysResult.supabaseAnonKey,
|
|
1227
1062
|
dbPassword,
|
|
1228
1063
|
};
|
|
1229
|
-
|
|
1064
|
+
ui.log.success(tr('new.supabase.existingLinked'));
|
|
1230
1065
|
}
|
|
1231
1066
|
}
|
|
1232
1067
|
}
|
|
@@ -1242,22 +1077,17 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1242
1077
|
let googleIosClientId = '';
|
|
1243
1078
|
|
|
1244
1079
|
if (backend === 'api') {
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
initial: 'https://api.example.com',
|
|
1252
|
-
},
|
|
1253
|
-
{ onCancel: cancel }
|
|
1254
|
-
);
|
|
1255
|
-
Object.assign(core, api);
|
|
1080
|
+
core.apiBaseUrl = await ui.text({
|
|
1081
|
+
message: tr('new.api.q.baseUrl'),
|
|
1082
|
+
placeholder: tr('new.api.q.baseUrl.hint'),
|
|
1083
|
+
initialValue: 'https://api.example.com',
|
|
1084
|
+
onCancel: cancel,
|
|
1085
|
+
});
|
|
1256
1086
|
}
|
|
1257
1087
|
|
|
1258
1088
|
// ── Firebase existing project: enable APIs + create Firestore/Storage ───
|
|
1259
1089
|
if (backend === 'firebase' && firebaseSetupMode === 'existing' && core.firebaseProjectId) {
|
|
1260
|
-
const ps4 =
|
|
1090
|
+
const ps4 = ui.makeStepper();
|
|
1261
1091
|
ps4.next(stepProgress('enable-apis', language));
|
|
1262
1092
|
const existingSetup = await setupExistingProject(core.firebaseProjectId, {
|
|
1263
1093
|
onProgress: (key, data) => {
|
|
@@ -1265,19 +1095,17 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1265
1095
|
ps4.next(stepProgress('enable-apis', language));
|
|
1266
1096
|
} else if (key === 'enable-apis-warn') {
|
|
1267
1097
|
ps4.warn(`${tr('new.firebase.create.failed')}: APIs`);
|
|
1268
|
-
|
|
1098
|
+
ui.log.warn(`${tr('new.firebase.existing.apisFailed')} ${(data?.error || '').slice(0, 80)}`);
|
|
1269
1099
|
} else if (key === 'firestore') {
|
|
1270
1100
|
ps4.next(stepProgress('firestore', language));
|
|
1271
1101
|
} else if (key === 'storage') {
|
|
1272
1102
|
ps4.next(stepProgress('storage', language));
|
|
1273
1103
|
} else if (key === 'auth-providers-warn') {
|
|
1274
1104
|
ps4.stop();
|
|
1275
|
-
|
|
1276
|
-
console.log(kleur.cyan(` ${data?.url || ''}`));
|
|
1105
|
+
ui.log.warn(`${tr('new.firebase.interactive.authWarn')}\n${kleur.cyan(data?.url || '')}`);
|
|
1277
1106
|
} else if (key === 'auth-google-warn') {
|
|
1278
1107
|
ps4.stop();
|
|
1279
|
-
|
|
1280
|
-
console.log(kleur.cyan(` ${data?.url || ''}`));
|
|
1108
|
+
ui.log.warn(`${tr('new.firebase.existing.googleSignInManual')}\n${kleur.cyan(data?.url || '')}`);
|
|
1281
1109
|
}
|
|
1282
1110
|
},
|
|
1283
1111
|
});
|
|
@@ -1285,17 +1113,15 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1285
1113
|
ps4.succeed(stepLabel('firestore', language));
|
|
1286
1114
|
} else if (existingSetup.firestoreError && !existingSetup.firestoreError.includes('ALREADY_EXISTS')) {
|
|
1287
1115
|
ps4.fail(`Firestore: ${(existingSetup.firestoreError || '').slice(0, 80)}`);
|
|
1288
|
-
|
|
1116
|
+
ui.log.message(kleur.cyan(existingSetup.firestoreUrl));
|
|
1289
1117
|
} else {
|
|
1290
1118
|
ps4.stop();
|
|
1291
1119
|
}
|
|
1292
1120
|
if (existingSetup.storageCreated) {
|
|
1293
|
-
|
|
1121
|
+
ui.log.success(stepLabel('storage', language));
|
|
1294
1122
|
} else if (existingSetup.storageError && !existingSetup.storageError.includes('ALREADY_EXISTS')) {
|
|
1295
|
-
|
|
1296
|
-
console.log(kleur.cyan(` ${existingSetup.storageUrl}`));
|
|
1123
|
+
ui.log.warn(`Storage: ${(existingSetup.storageError || '').slice(0, 80)}\n${kleur.cyan(existingSetup.storageUrl)}`);
|
|
1297
1124
|
}
|
|
1298
|
-
console.log('');
|
|
1299
1125
|
}
|
|
1300
1126
|
|
|
1301
1127
|
// ── Optional modules ────────────────────────────────────────────────────
|
|
@@ -1311,22 +1137,18 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1311
1137
|
modules = preselectedModules;
|
|
1312
1138
|
} else if (isQuick) {
|
|
1313
1139
|
// Quick mode: show preset picker (no multiselect).
|
|
1314
|
-
const
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
initial: 0,
|
|
1327
|
-
},
|
|
1328
|
-
{ onCancel: cancel }
|
|
1329
|
-
);
|
|
1140
|
+
const preset = await ui.select({
|
|
1141
|
+
message: tr('new.q.preset'),
|
|
1142
|
+
initialValue: 'starter',
|
|
1143
|
+
options: [
|
|
1144
|
+
{ value: 'starter', label: tr('new.q.preset.starter') },
|
|
1145
|
+
{ value: 'saas', label: tr('new.q.preset.saas') },
|
|
1146
|
+
{ value: 'content', label: tr('new.q.preset.content') },
|
|
1147
|
+
{ value: 'full', label: tr('new.q.preset.full') },
|
|
1148
|
+
{ value: 'none', label: tr('new.q.preset.none') },
|
|
1149
|
+
],
|
|
1150
|
+
onCancel: cancel,
|
|
1151
|
+
});
|
|
1330
1152
|
modules = MODULE_PRESETS[preset] || [];
|
|
1331
1153
|
} else {
|
|
1332
1154
|
// Advanced mode: full multiselect — built from catalog, filtered by audience + backend.
|
|
@@ -1344,7 +1166,9 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1344
1166
|
{ header: 'new.modules.header.ci', ids: ['ci'] },
|
|
1345
1167
|
];
|
|
1346
1168
|
|
|
1347
|
-
|
|
1169
|
+
// Clack multiselect does not support disabled separator rows, so we
|
|
1170
|
+
// prefix each option with its group name (e.g. "Monetização · RevenueCat").
|
|
1171
|
+
const moduleOptions = [];
|
|
1348
1172
|
for (const group of groups) {
|
|
1349
1173
|
const inGroup = visibleFeatures.filter((f) => {
|
|
1350
1174
|
if (!group.ids.includes(f.id)) return false;
|
|
@@ -1352,29 +1176,24 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1352
1176
|
return true;
|
|
1353
1177
|
});
|
|
1354
1178
|
if (inGroup.length === 0) continue;
|
|
1355
|
-
|
|
1356
|
-
moduleChoices.push({ title: tr(group.header), value: `__header_${group.header}__`, disabled: true });
|
|
1357
|
-
}
|
|
1179
|
+
const groupLabel = group.header ? tr(group.header) : null;
|
|
1358
1180
|
for (const f of inGroup) {
|
|
1359
|
-
const
|
|
1360
|
-
|
|
1181
|
+
const moduleLabel = tr(`new.firebase.module.${f.id}`) + (f.status === 'internal' ? ' [beta]' : '');
|
|
1182
|
+
const label = groupLabel
|
|
1183
|
+
? `${kleur.dim(groupLabel + ' · ')}${moduleLabel}`
|
|
1184
|
+
: moduleLabel;
|
|
1185
|
+
moduleOptions.push({ value: f.id, label });
|
|
1361
1186
|
}
|
|
1362
1187
|
}
|
|
1363
1188
|
|
|
1364
|
-
const
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
min: 0,
|
|
1373
|
-
warn: tr('prompt.multiselect.warnDisabled'),
|
|
1374
|
-
},
|
|
1375
|
-
{ onCancel: cancel }
|
|
1376
|
-
);
|
|
1377
|
-
modules = (rawModules || []).filter((m) => !String(m).startsWith('__header_'));
|
|
1189
|
+
const rawModules = await ui.multiselect({
|
|
1190
|
+
message: tr('new.firebase.q.modules'),
|
|
1191
|
+
options: moduleOptions,
|
|
1192
|
+
initialValues: [],
|
|
1193
|
+
required: false,
|
|
1194
|
+
onCancel: cancel,
|
|
1195
|
+
});
|
|
1196
|
+
modules = rawModules || [];
|
|
1378
1197
|
}
|
|
1379
1198
|
|
|
1380
1199
|
if (backend === 'firebase' && firebaseSetupMode === 'create' && firebaseIncludeWeb) {
|
|
@@ -1385,57 +1204,41 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1385
1204
|
const moduleAnswers = {};
|
|
1386
1205
|
|
|
1387
1206
|
if (modules.includes('revenuecat')) {
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
{
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
hint: tr('new.firebase.q.paywall.hint'),
|
|
1407
|
-
choices: [
|
|
1408
|
-
{ title: 'Basic (list of plans)', value: 'basic' },
|
|
1409
|
-
{ title: 'With trial switch', value: 'withSwitch' },
|
|
1410
|
-
{ title: 'Row + comparison table', value: 'basicRow' },
|
|
1411
|
-
{ title: 'Minimal (benefits + CTA)', value: 'minimal' },
|
|
1412
|
-
],
|
|
1413
|
-
initial: 0,
|
|
1414
|
-
},
|
|
1207
|
+
moduleAnswers.rcAndroidKey = await ui.text({
|
|
1208
|
+
message: tr('new.firebase.q.revenuecat.android'),
|
|
1209
|
+
validate: (v) => (v && v.trim() ? undefined : tr('new.firebase.q.revenuecat.android.required')),
|
|
1210
|
+
onCancel: cancel,
|
|
1211
|
+
});
|
|
1212
|
+
moduleAnswers.rcIosKey = await ui.text({
|
|
1213
|
+
message: tr('new.firebase.q.revenuecat.ios'),
|
|
1214
|
+
validate: (v) => (v && v.trim() ? undefined : tr('new.firebase.q.revenuecat.ios.required')),
|
|
1215
|
+
onCancel: cancel,
|
|
1216
|
+
});
|
|
1217
|
+
moduleAnswers.defaultPaywall = await ui.select({
|
|
1218
|
+
message: tr('new.firebase.q.paywall'),
|
|
1219
|
+
initialValue: 'basic',
|
|
1220
|
+
options: [
|
|
1221
|
+
{ value: 'basic', label: 'Basic (list of plans)' },
|
|
1222
|
+
{ value: 'withSwitch', label: 'With trial switch' },
|
|
1223
|
+
{ value: 'basicRow', label: 'Row + comparison table' },
|
|
1224
|
+
{ value: 'minimal', label: 'Minimal (benefits + CTA)' },
|
|
1415
1225
|
],
|
|
1416
|
-
|
|
1417
|
-
);
|
|
1418
|
-
Object.assign(moduleAnswers, rc);
|
|
1226
|
+
onCancel: cancel,
|
|
1227
|
+
});
|
|
1419
1228
|
}
|
|
1420
1229
|
|
|
1421
1230
|
// RC web key — only in advanced mode (optional credential, can configure later).
|
|
1422
1231
|
if (!isQuick && modules.includes('revenuecat') && modules.includes('web')) {
|
|
1423
|
-
const rcWebKey = await
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
validate: (v) => {
|
|
1429
|
-
if (!v || !v.trim()) return true; // optional — blank is fine
|
|
1430
|
-
return /^rcb_/.test(v.trim()) ? true : tr('new.firebase.q.revenuecat.webKey.invalid');
|
|
1431
|
-
},
|
|
1232
|
+
const rcWebKey = await ui.text({
|
|
1233
|
+
message: tr('new.firebase.q.revenuecat.webKey'),
|
|
1234
|
+
validate: (v) => {
|
|
1235
|
+
if (!v || !v.trim()) return undefined; // optional — blank is fine
|
|
1236
|
+
return /^rcb_/.test(v.trim()) ? undefined : tr('new.firebase.q.revenuecat.webKey.invalid');
|
|
1432
1237
|
},
|
|
1433
|
-
|
|
1434
|
-
);
|
|
1435
|
-
Object.assign(moduleAnswers, {
|
|
1436
|
-
revenuecatWeb: true,
|
|
1437
|
-
rcWebKey: (rcWebKey.rcWebKey || '').trim(),
|
|
1238
|
+
onCancel: cancel,
|
|
1438
1239
|
});
|
|
1240
|
+
moduleAnswers.revenuecatWeb = true;
|
|
1241
|
+
moduleAnswers.rcWebKey = (rcWebKey || '').trim();
|
|
1439
1242
|
} else if (modules.includes('revenuecat') && modules.includes('web')) {
|
|
1440
1243
|
// Quick mode: mark web billing as included but key will be configured later.
|
|
1441
1244
|
moduleAnswers.revenuecatWeb = true;
|
|
@@ -1444,76 +1247,52 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1444
1247
|
|
|
1445
1248
|
// Sentry DSN — already optional (blank = configure later). Skip in quick mode.
|
|
1446
1249
|
if (!isQuick && modules.includes('sentry')) {
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
return /^https?:\/\/[^@\s]+@[^/\s]+\/\d+$/.test(v.trim())
|
|
1455
|
-
? true
|
|
1456
|
-
: tr('new.firebase.q.sentry.dsn.invalid');
|
|
1457
|
-
},
|
|
1250
|
+
moduleAnswers.sentryDsn = await ui.text({
|
|
1251
|
+
message: tr('new.firebase.q.sentry.dsn'),
|
|
1252
|
+
validate: (v) => {
|
|
1253
|
+
if (!v || !v.trim()) return undefined; // optional — blank is fine
|
|
1254
|
+
return /^https?:\/\/[^@\s]+@[^/\s]+\/\d+$/.test(v.trim())
|
|
1255
|
+
? undefined
|
|
1256
|
+
: tr('new.firebase.q.sentry.dsn.invalid');
|
|
1458
1257
|
},
|
|
1459
|
-
|
|
1460
|
-
);
|
|
1461
|
-
Object.assign(moduleAnswers, s);
|
|
1258
|
+
onCancel: cancel,
|
|
1259
|
+
});
|
|
1462
1260
|
}
|
|
1463
1261
|
|
|
1464
1262
|
// Mixpanel token — already optional. Skip in quick mode.
|
|
1465
1263
|
if (!isQuick && modules.includes('analytics')) {
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
message: tr('new.firebase.q.mixpanel.token'),
|
|
1471
|
-
},
|
|
1472
|
-
{ onCancel: cancel }
|
|
1473
|
-
);
|
|
1474
|
-
Object.assign(moduleAnswers, mx);
|
|
1264
|
+
moduleAnswers.mixpanelToken = await ui.text({
|
|
1265
|
+
message: tr('new.firebase.q.mixpanel.token'),
|
|
1266
|
+
onCancel: cancel,
|
|
1267
|
+
});
|
|
1475
1268
|
}
|
|
1476
1269
|
|
|
1477
1270
|
// LLM Chat credentials — skip in quick mode, all fields optional.
|
|
1478
1271
|
if (!isQuick && modules.includes('llm_chat')) {
|
|
1479
|
-
const
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
hint: tr('new.q.llm_chat.configureNow.hint'),
|
|
1485
|
-
initial: true,
|
|
1486
|
-
},
|
|
1487
|
-
{ onCancel: cancel }
|
|
1488
|
-
);
|
|
1272
|
+
const configureLlmNow = await ui.confirm({
|
|
1273
|
+
message: tr('new.q.llm_chat.configureNow'),
|
|
1274
|
+
initialValue: true,
|
|
1275
|
+
onCancel: cancel,
|
|
1276
|
+
});
|
|
1489
1277
|
|
|
1490
1278
|
if (configureLlmNow) {
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
choices: [
|
|
1498
|
-
{ title: 'OpenAI (gpt-4o-mini)', value: 'openai' },
|
|
1499
|
-
{ title: 'Google Gemini (gemini-1.5-flash)', value: 'gemini' },
|
|
1500
|
-
],
|
|
1501
|
-
initial: 0,
|
|
1502
|
-
},
|
|
1503
|
-
{
|
|
1504
|
-
type: 'text',
|
|
1505
|
-
name: 'llmSystemPrompt',
|
|
1506
|
-
message: tr('add.prompt.llmSystemPrompt'),
|
|
1507
|
-
},
|
|
1508
|
-
{
|
|
1509
|
-
type: 'text',
|
|
1510
|
-
name: 'llmApiKey',
|
|
1511
|
-
message: tr('add.prompt.llmApiKey'),
|
|
1512
|
-
},
|
|
1279
|
+
moduleAnswers.llmProvider = await ui.select({
|
|
1280
|
+
message: tr('add.prompt.llmProvider'),
|
|
1281
|
+
initialValue: 'openai',
|
|
1282
|
+
options: [
|
|
1283
|
+
{ value: 'openai', label: 'OpenAI (gpt-4o-mini)' },
|
|
1284
|
+
{ value: 'gemini', label: 'Google Gemini (gemini-1.5-flash)' },
|
|
1513
1285
|
],
|
|
1514
|
-
|
|
1515
|
-
);
|
|
1516
|
-
|
|
1286
|
+
onCancel: cancel,
|
|
1287
|
+
});
|
|
1288
|
+
moduleAnswers.llmSystemPrompt = await ui.text({
|
|
1289
|
+
message: tr('add.prompt.llmSystemPrompt'),
|
|
1290
|
+
onCancel: cancel,
|
|
1291
|
+
});
|
|
1292
|
+
moduleAnswers.llmApiKey = await ui.password({
|
|
1293
|
+
message: tr('add.prompt.llmApiKey'),
|
|
1294
|
+
onCancel: cancel,
|
|
1295
|
+
});
|
|
1517
1296
|
} else {
|
|
1518
1297
|
moduleAnswers.llmConfigureLater = true;
|
|
1519
1298
|
}
|
|
@@ -1523,70 +1302,48 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1523
1302
|
|
|
1524
1303
|
// Facebook — required credentials, always ask.
|
|
1525
1304
|
if (modules.includes('facebook')) {
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
name: 'fbToken',
|
|
1540
|
-
message: tr('new.firebase.q.facebook.token'),
|
|
1541
|
-
validate: (v) => (v && v.trim() ? true : tr('new.firebase.q.facebook.token.required')),
|
|
1542
|
-
},
|
|
1543
|
-
],
|
|
1544
|
-
{ onCancel: cancel }
|
|
1545
|
-
);
|
|
1546
|
-
Object.assign(moduleAnswers, fb);
|
|
1305
|
+
moduleAnswers.fbAppId = await ui.text({
|
|
1306
|
+
message: tr('new.firebase.q.facebook.appId'),
|
|
1307
|
+
validate: (v) => {
|
|
1308
|
+
if (!v || !v.trim()) return tr('new.firebase.q.facebook.appId.required');
|
|
1309
|
+
return /^\d+$/.test(v.trim()) ? undefined : tr('new.firebase.q.facebook.appId.invalid');
|
|
1310
|
+
},
|
|
1311
|
+
onCancel: cancel,
|
|
1312
|
+
});
|
|
1313
|
+
moduleAnswers.fbToken = await ui.password({
|
|
1314
|
+
message: tr('new.firebase.q.facebook.token'),
|
|
1315
|
+
validate: (v) => (v && v.trim() ? undefined : tr('new.firebase.q.facebook.token.required')),
|
|
1316
|
+
onCancel: cancel,
|
|
1317
|
+
});
|
|
1547
1318
|
}
|
|
1548
1319
|
|
|
1549
1320
|
// Server secrets (webhook, Meta Ads) — skip in quick mode, configure later via `kasy deploy`.
|
|
1550
1321
|
if (!isQuick && modules.includes('revenuecat') && (backend === 'supabase' || backend === 'firebase')) {
|
|
1551
|
-
const
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
hint: tr('new.firebase.q.secrets.configureNow.hint'),
|
|
1557
|
-
initial: true,
|
|
1558
|
-
},
|
|
1559
|
-
{ onCancel: cancel }
|
|
1560
|
-
);
|
|
1322
|
+
const configureSecretsNow = await ui.confirm({
|
|
1323
|
+
message: tr('new.firebase.q.secrets.configureNow'),
|
|
1324
|
+
initialValue: true,
|
|
1325
|
+
onCancel: cancel,
|
|
1326
|
+
});
|
|
1561
1327
|
|
|
1562
1328
|
if (configureSecretsNow) {
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
validate: (v) => {
|
|
1582
|
-
if (!v || !v.trim()) return true; // optional — blank is fine
|
|
1583
|
-
return /^\d+$/.test(v.trim()) ? true : tr('new.firebase.q.revenuecat.metaDataset.invalid');
|
|
1584
|
-
},
|
|
1585
|
-
},
|
|
1586
|
-
],
|
|
1587
|
-
{ onCancel: cancel }
|
|
1588
|
-
);
|
|
1589
|
-
Object.assign(moduleAnswers, rcSecrets);
|
|
1329
|
+
moduleAnswers.rcWebhookKey = await ui.text({
|
|
1330
|
+
message: tr('new.firebase.q.revenuecat.webhookKey'),
|
|
1331
|
+
initialValue: generateWebhookKey(),
|
|
1332
|
+
placeholder: tr('new.firebase.q.revenuecat.webhookKey.hint'),
|
|
1333
|
+
onCancel: cancel,
|
|
1334
|
+
});
|
|
1335
|
+
moduleAnswers.metaAccessToken = await ui.password({
|
|
1336
|
+
message: tr('new.firebase.q.revenuecat.metaToken'),
|
|
1337
|
+
onCancel: cancel,
|
|
1338
|
+
});
|
|
1339
|
+
moduleAnswers.metaDatasetId = await ui.text({
|
|
1340
|
+
message: tr('new.firebase.q.revenuecat.metaDataset'),
|
|
1341
|
+
validate: (v) => {
|
|
1342
|
+
if (!v || !v.trim()) return undefined; // optional — blank is fine
|
|
1343
|
+
return /^\d+$/.test(v.trim()) ? undefined : tr('new.firebase.q.revenuecat.metaDataset.invalid');
|
|
1344
|
+
},
|
|
1345
|
+
onCancel: cancel,
|
|
1346
|
+
});
|
|
1590
1347
|
} else {
|
|
1591
1348
|
moduleAnswers.secretsConfigureLater = true;
|
|
1592
1349
|
}
|
|
@@ -1616,24 +1373,29 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1616
1373
|
printSummary(tr, answers);
|
|
1617
1374
|
|
|
1618
1375
|
if (!yes) {
|
|
1619
|
-
const
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
initial: true,
|
|
1625
|
-
},
|
|
1626
|
-
{ onCancel: cancel }
|
|
1627
|
-
);
|
|
1376
|
+
const proceed = await ui.confirm({
|
|
1377
|
+
message: tr('new.firebase.confirm.proceed'),
|
|
1378
|
+
initialValue: true,
|
|
1379
|
+
onCancel: cancel,
|
|
1380
|
+
});
|
|
1628
1381
|
if (!proceed) {
|
|
1629
|
-
|
|
1382
|
+
ui.cancel(tr('prompt.cancelled'));
|
|
1630
1383
|
process.exit(0);
|
|
1631
1384
|
}
|
|
1632
1385
|
}
|
|
1633
1386
|
|
|
1634
1387
|
// ── Generate ────────────────────────────────────────────────────────────
|
|
1635
|
-
|
|
1636
|
-
|
|
1388
|
+
// Stepper shows each step closing as ✦ and the next starting as ⠙ — so the
|
|
1389
|
+
// user gets explicit "X done → Y starting" feedback instead of a single
|
|
1390
|
+
// spinner with a mutating message.
|
|
1391
|
+
const stepper = ui.makeStepper();
|
|
1392
|
+
// First step started here so even silent prep work shows progress.
|
|
1393
|
+
stepper.next(stepProgress('project-setup', language));
|
|
1394
|
+
|
|
1395
|
+
const onProgress = (key) => {
|
|
1396
|
+
// Each onProgress closes previous step with ✦ and starts the new one.
|
|
1397
|
+
stepper.next(stepProgress(key, language));
|
|
1398
|
+
};
|
|
1637
1399
|
|
|
1638
1400
|
let result;
|
|
1639
1401
|
try {
|
|
@@ -1647,9 +1409,7 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1647
1409
|
modules: answers.modules,
|
|
1648
1410
|
moduleAnswers,
|
|
1649
1411
|
language,
|
|
1650
|
-
onProgress
|
|
1651
|
-
spinner.text = kleur.cyan(stepProgress(key, language));
|
|
1652
|
-
},
|
|
1412
|
+
onProgress,
|
|
1653
1413
|
});
|
|
1654
1414
|
} else if (backend === 'api') {
|
|
1655
1415
|
result = await generateApiProject(targetDir, {
|
|
@@ -1660,9 +1420,7 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1660
1420
|
modules: answers.modules,
|
|
1661
1421
|
moduleAnswers,
|
|
1662
1422
|
language,
|
|
1663
|
-
onProgress
|
|
1664
|
-
spinner.text = kleur.cyan(stepProgress(key, language));
|
|
1665
|
-
},
|
|
1423
|
+
onProgress,
|
|
1666
1424
|
});
|
|
1667
1425
|
} else {
|
|
1668
1426
|
result = await generateFirebaseProject(targetDir, {
|
|
@@ -1675,14 +1433,14 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1675
1433
|
includeWeb: answers.includeWeb !== false,
|
|
1676
1434
|
functionsRegion: firebaseRegion,
|
|
1677
1435
|
language,
|
|
1678
|
-
onProgress
|
|
1679
|
-
spinner.text = kleur.cyan(stepProgress(key, language));
|
|
1680
|
-
},
|
|
1436
|
+
onProgress,
|
|
1681
1437
|
});
|
|
1682
1438
|
}
|
|
1683
|
-
|
|
1439
|
+
// Close the last in-flight step. We don't need a label — its own message
|
|
1440
|
+
// is already shown next to the ✦ by Clack.
|
|
1441
|
+
stepper.succeed();
|
|
1684
1442
|
} catch (err) {
|
|
1685
|
-
|
|
1443
|
+
stepper.fail(err.message);
|
|
1686
1444
|
throw err;
|
|
1687
1445
|
}
|
|
1688
1446
|
|
|
@@ -1709,8 +1467,12 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1709
1467
|
}
|
|
1710
1468
|
|
|
1711
1469
|
// ── Results ─────────────────────────────────────────────────────────────
|
|
1712
|
-
|
|
1713
|
-
|
|
1470
|
+
// Stepper already announced each successful step. Print only failures and
|
|
1471
|
+
// skipped steps here, so the user notices what didn't run without seeing
|
|
1472
|
+
// every success twice.
|
|
1473
|
+
result.steps
|
|
1474
|
+
.filter((s) => s.skipped || !s.ok)
|
|
1475
|
+
.forEach((s) => printStepResult(s, language));
|
|
1714
1476
|
|
|
1715
1477
|
// ── Supabase: link + db push + functions deploy (when project was created or existing selected) ─
|
|
1716
1478
|
const supabaseSetupPayload = (supabaseCreate && supabaseCreateResult?.ok)
|
|
@@ -1750,19 +1512,20 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1750
1512
|
// ── FCM Service Account key (best effort via gcloud — Firebase uses ADC, Supabase needs JSON) ──
|
|
1751
1513
|
let fcmServiceAccountJson = null;
|
|
1752
1514
|
if (answers.firebaseProjectId) {
|
|
1753
|
-
const fcmSpinner =
|
|
1515
|
+
const fcmSpinner = ui.spinner();
|
|
1516
|
+
fcmSpinner.start(tr('new.fcm.generating'));
|
|
1754
1517
|
const fcmResult = await createFcmServiceAccountKey(answers.firebaseProjectId);
|
|
1518
|
+
fcmSpinner.stop(tr('new.fcm.generating'));
|
|
1755
1519
|
if (fcmResult.ok) {
|
|
1756
1520
|
fcmServiceAccountJson = fcmResult.json;
|
|
1757
|
-
fcmSpinner.stop();
|
|
1758
1521
|
printStepResult({ name: 'fcm-key', ok: true }, language);
|
|
1759
1522
|
} else {
|
|
1760
|
-
fcmSpinner.stop();
|
|
1761
1523
|
printStepResult({ name: 'fcm-key', ok: false, detail: 'configure FIREBASE_SERVICE_ACCOUNT_JSON manualmente' }, language);
|
|
1762
1524
|
}
|
|
1763
1525
|
}
|
|
1764
1526
|
|
|
1765
|
-
const setupSpinner =
|
|
1527
|
+
const setupSpinner = ui.spinner();
|
|
1528
|
+
setupSpinner.start(tr('new.supabase.setup'));
|
|
1766
1529
|
try {
|
|
1767
1530
|
const secrets = {
|
|
1768
1531
|
firebaseProjectId: answers.firebaseProjectId,
|
|
@@ -1788,21 +1551,22 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1788
1551
|
secrets,
|
|
1789
1552
|
google
|
|
1790
1553
|
);
|
|
1791
|
-
setupSpinner.stop();
|
|
1554
|
+
setupSpinner.stop(tr('new.supabase.setup'));
|
|
1792
1555
|
setupSteps.forEach((s) => printStepResult(s, language));
|
|
1793
1556
|
} catch (err) {
|
|
1794
|
-
setupSpinner.
|
|
1795
|
-
|
|
1557
|
+
setupSpinner.error(err.message);
|
|
1558
|
+
ui.log.warn(tr('new.supabase.setupManual'));
|
|
1796
1559
|
}
|
|
1797
1560
|
}
|
|
1798
1561
|
}
|
|
1799
1562
|
|
|
1800
1563
|
// ── API: FCM Service Account key — save to .kasy/ for server configuration ──
|
|
1801
1564
|
if (backend === 'api' && answers.firebaseProjectId) {
|
|
1802
|
-
const fcmSpinner =
|
|
1565
|
+
const fcmSpinner = ui.spinner();
|
|
1566
|
+
fcmSpinner.start(tr('new.fcm.generating'));
|
|
1803
1567
|
const fcmResult = await createFcmServiceAccountKey(answers.firebaseProjectId);
|
|
1568
|
+
fcmSpinner.stop(tr('new.fcm.generating'));
|
|
1804
1569
|
if (fcmResult.ok) {
|
|
1805
|
-
fcmSpinner.stop();
|
|
1806
1570
|
try {
|
|
1807
1571
|
const kasyDir = path.join(targetDir, '.kasy');
|
|
1808
1572
|
await fs.ensureDir(kasyDir);
|
|
@@ -1817,12 +1581,11 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1817
1581
|
}
|
|
1818
1582
|
}
|
|
1819
1583
|
printStepResult({ name: 'fcm-key-saved', ok: true }, language);
|
|
1820
|
-
|
|
1584
|
+
ui.log.message(tr('new.fcm.serverConfig'));
|
|
1821
1585
|
} catch (_) {
|
|
1822
1586
|
printStepResult({ name: 'fcm-key-saved', ok: false, detail: 'salvar arquivo de chave falhou' }, language);
|
|
1823
1587
|
}
|
|
1824
1588
|
} else {
|
|
1825
|
-
fcmSpinner.stop();
|
|
1826
1589
|
printStepResult({ name: 'fcm-key', ok: false, detail: 'configure FIREBASE_SERVICE_ACCOUNT_JSON manualmente' }, language);
|
|
1827
1590
|
}
|
|
1828
1591
|
}
|
|
@@ -1835,9 +1598,10 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1835
1598
|
if (answers.firebaseProjectId && firebaseSetupMode === 'existing') {
|
|
1836
1599
|
const flutterfireOkForSha1 = result.steps.find((s) => s.name === 'flutterfire')?.ok;
|
|
1837
1600
|
if (flutterfireOkForSha1) {
|
|
1838
|
-
const sha1Spinner =
|
|
1601
|
+
const sha1Spinner = ui.spinner();
|
|
1602
|
+
sha1Spinner.start(tr('new.sha1.registering'));
|
|
1839
1603
|
const sha1Result = await registerDebugSha1(answers.firebaseProjectId, answers.bundleId);
|
|
1840
|
-
sha1Spinner.stop();
|
|
1604
|
+
sha1Spinner.stop(tr('new.sha1.registering'));
|
|
1841
1605
|
if (sha1Result.ok) {
|
|
1842
1606
|
if (!sha1Result.existed) {
|
|
1843
1607
|
printStepResult({ name: 'sha1', ok: true }, language);
|
|
@@ -1845,9 +1609,9 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1845
1609
|
// existed === true → already registered, silent success
|
|
1846
1610
|
} else {
|
|
1847
1611
|
const sha1ManualUrl = `https://console.firebase.google.com/project/${answers.firebaseProjectId}/settings/general/android:${answers.bundleId}`;
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1612
|
+
ui.log.warn(
|
|
1613
|
+
`${tr('new.sha1.failed', { error: (sha1Result.sha1Error || '').slice(0, 120) })}\n${tr('new.sha1.manual')}\n${kleur.cyan(sha1ManualUrl)}`
|
|
1614
|
+
);
|
|
1851
1615
|
openUrl(sha1ManualUrl);
|
|
1852
1616
|
}
|
|
1853
1617
|
}
|
|
@@ -1856,14 +1620,9 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1856
1620
|
const flutterfireStep = result.steps.find((s) => s.name === 'flutterfire');
|
|
1857
1621
|
if (flutterfireStep && !flutterfireStep.ok) {
|
|
1858
1622
|
const platforms = answers.includeWeb !== false ? 'android,ios,web' : 'android,ios';
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
kleur.cyan(
|
|
1863
|
-
` dart pub global run flutterfire_cli:flutterfire configure` +
|
|
1864
|
-
` --project=${answers.firebaseProjectId}` +
|
|
1865
|
-
` --out lib/firebase_options_dev.dart --platforms=${platforms} --yes`
|
|
1866
|
-
)
|
|
1623
|
+
const cmd = `dart pub global run flutterfire_cli:flutterfire configure --project=${answers.firebaseProjectId} --out lib/firebase_options_dev.dart --platforms=${platforms} --yes`;
|
|
1624
|
+
ui.log.warn(
|
|
1625
|
+
`${tr('new.firebase.warn.flutterfire')}\n${tr('new.firebase.warn.flutterfire.manual')}\n${kleur.cyan(cmd)}`
|
|
1867
1626
|
);
|
|
1868
1627
|
}
|
|
1869
1628
|
|
|
@@ -1871,16 +1630,15 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1871
1630
|
// Cannot be automated: the .p8 key only exists in Apple Developer Portal.
|
|
1872
1631
|
if (answers.firebaseProjectId) {
|
|
1873
1632
|
const apnsUrl = `https://console.firebase.google.com/project/${answers.firebaseProjectId}/settings/cloudmessaging`;
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
console.log(kleur.gray(` ${tr('new.apns.step2')}`));
|
|
1878
|
-
console.log(kleur.cyan(` ${apnsUrl}`));
|
|
1633
|
+
ui.log.warn(
|
|
1634
|
+
`${tr('new.apns.warning')}\n${tr('new.apns.step1')}\n${tr('new.apns.step2')}\n${kleur.cyan(apnsUrl)}`
|
|
1635
|
+
);
|
|
1879
1636
|
openUrl(apnsUrl);
|
|
1880
1637
|
}
|
|
1881
1638
|
|
|
1882
1639
|
printSuccessCard(tr, answers, targetDir);
|
|
1883
1640
|
|
|
1641
|
+
ui.outro(kleur.bold(tr('new.success.title')));
|
|
1884
1642
|
}
|
|
1885
1643
|
|
|
1886
1644
|
module.exports = { runNew };
|