kasy-cli 1.5.2 → 1.6.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 +5 -2
- package/lib/commands/add.js +101 -100
- 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 +20 -17
- package/lib/commands/ios.js +69 -69
- package/lib/commands/new.js +662 -913
- package/lib/commands/notifications.js +59 -59
- package/lib/commands/remove.js +28 -27
- package/lib/commands/run.js +5 -2
- package/lib/commands/update.js +104 -96
- package/lib/commands/validate.js +24 -19
- package/lib/utils/apple-release.js +23 -11
- package/lib/utils/brand.js +72 -0
- package/lib/utils/checks.js +39 -10
- package/lib/utils/i18n.js +21 -3
- package/lib/utils/prompts.js +82 -142
- package/lib/utils/ui.js +174 -0
- package/lib/utils/updates.js +17 -18
- package/package.json +3 -1
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,7 +40,8 @@ function openUrl(url) {
|
|
|
68
40
|
exec(cmd, { shell: true });
|
|
69
41
|
} catch (_) {}
|
|
70
42
|
}
|
|
71
|
-
const
|
|
43
|
+
const ui = require('../utils/ui');
|
|
44
|
+
const { printBanner, infoBox, successBox } = require('../utils/brand');
|
|
72
45
|
const fs = require('fs-extra');
|
|
73
46
|
const { createTranslator } = require('../utils/i18n');
|
|
74
47
|
const { getStoredLanguage, setStoredLanguage } = require('../utils/license');
|
|
@@ -110,51 +83,37 @@ const BILLING_OTHER = '__other__';
|
|
|
110
83
|
async function promptBillingAccountIfNeeded(tr, onCancel) {
|
|
111
84
|
const billingList = await listBillingAccounts();
|
|
112
85
|
if (!billingList.ok) return null;
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
validate: (v) => (v && v.trim() ? true : tr('new.firebase.q.billingAccount.manualId.required')),
|
|
122
|
-
},
|
|
123
|
-
{ onCancel }
|
|
124
|
-
);
|
|
125
|
-
return manualId?.trim() || null;
|
|
126
|
-
}
|
|
127
|
-
const choices = [
|
|
128
|
-
...billingList.accounts.map((a) => ({
|
|
129
|
-
title: `${a.name || a.id} (${a.id})`,
|
|
130
|
-
value: a.id,
|
|
131
|
-
})),
|
|
132
|
-
{ title: tr('new.firebase.q.billingAccount.other'), value: BILLING_OTHER },
|
|
133
|
-
];
|
|
134
|
-
console.log(kleur.yellow(` ${tr('new.firebase.q.billingAccount.context')}`));
|
|
135
|
-
const { billingAccountId } = await prompts(
|
|
136
|
-
{
|
|
137
|
-
type: 'select',
|
|
138
|
-
name: 'billingAccountId',
|
|
139
|
-
message: tr('new.firebase.q.billingAccount'),
|
|
140
|
-
hint: tr('new.firebase.q.billingAccount.hint'),
|
|
141
|
-
choices,
|
|
142
|
-
},
|
|
143
|
-
{ onCancel }
|
|
144
|
-
);
|
|
145
|
-
if (billingAccountId === BILLING_OTHER) {
|
|
146
|
-
const { manualId } = await prompts(
|
|
147
|
-
{
|
|
148
|
-
type: 'text',
|
|
149
|
-
name: 'manualId',
|
|
150
|
-
message: tr('new.firebase.q.billingAccount.manualId'),
|
|
151
|
-
hint: tr('new.firebase.q.billingAccount.manualId.hint'),
|
|
152
|
-
validate: (v) => (v && v.trim() ? true : tr('new.firebase.q.billingAccount.manualId.required')),
|
|
153
|
-
},
|
|
154
|
-
{ onCancel }
|
|
155
|
-
);
|
|
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
|
+
});
|
|
156
94
|
return manualId?.trim() || null;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
if (!billingList.accounts?.length) {
|
|
98
|
+
ui.note(tr('new.firebase.q.billingAccount.context'));
|
|
99
|
+
return askManualId();
|
|
157
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();
|
|
158
117
|
return billingAccountId;
|
|
159
118
|
}
|
|
160
119
|
|
|
@@ -168,100 +127,77 @@ async function promptBillingAccountIfNeeded(tr, onCancel) {
|
|
|
168
127
|
async function promptOrganizationIfNeeded(tr, onCancel) {
|
|
169
128
|
const orgList = await listGcpOrganizations();
|
|
170
129
|
if (!orgList.ok || !orgList.organizations?.length) return null;
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
hint: tr('new.firebase.q.organization.hint'),
|
|
184
|
-
choices,
|
|
185
|
-
},
|
|
186
|
-
{ onCancel }
|
|
187
|
-
);
|
|
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
|
+
});
|
|
188
142
|
return organizationId || null;
|
|
189
143
|
}
|
|
190
144
|
|
|
191
145
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
192
146
|
|
|
193
|
-
function printBanner(tr) {
|
|
194
|
-
const bar = kleur.gray('─────────────────────────────────────────────────');
|
|
195
|
-
const logo = [
|
|
196
|
-
' ╦╔═ ╔═╗ ╔═╗ ╦ ╦',
|
|
197
|
-
' ╠╩╗ ╠═╣ ╚═╗ ╚╦╝',
|
|
198
|
-
' ╩ ╩ ╩ ╩ ╚═╝ ╩ ',
|
|
199
|
-
]
|
|
200
|
-
.map((line) => gradient(['#a78bfa', '#60a5fa'])(line))
|
|
201
|
-
.join('\n');
|
|
202
|
-
console.log(`\n${bar}\n`);
|
|
203
|
-
console.log(logo);
|
|
204
|
-
console.log('');
|
|
205
|
-
console.log(` ${kleur.dim(tr('new.subtitle2'))}`);
|
|
206
|
-
console.log(`\n${bar}\n`);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
147
|
function printPrerequisites(tr, backend, firebaseSetupMode = 'existing', checkResults = []) {
|
|
210
148
|
const gcloudOk = checkResults.every(
|
|
211
149
|
(r) => !r.name?.includes('gcloud') || r.ok
|
|
212
150
|
);
|
|
213
|
-
console.log(kleur.bold().yellow(` ${tr('new.prereq.title')}`));
|
|
214
151
|
const firebaseCreate = firebaseSetupMode === 'create';
|
|
152
|
+
const items = [];
|
|
153
|
+
|
|
215
154
|
if (backend === 'firebase') {
|
|
216
155
|
if (firebaseCreate) {
|
|
217
|
-
if (!gcloudOk)
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
console.log(kleur.gray(` ${tr('new.firebase.prereq.create.projectQuota')}`));
|
|
222
|
-
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'));
|
|
223
160
|
} else {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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'));
|
|
230
167
|
}
|
|
231
168
|
} else if (backend === 'supabase') {
|
|
232
169
|
if (firebaseCreate) {
|
|
233
|
-
if (!gcloudOk)
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
console.log(kleur.gray(` ${tr('new.supabase.prereq.2')}`));
|
|
241
|
-
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'));
|
|
242
177
|
} else {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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'));
|
|
247
182
|
}
|
|
248
183
|
} else if (backend === 'api') {
|
|
249
184
|
if (firebaseCreate) {
|
|
250
|
-
if (!gcloudOk)
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
console.log(kleur.gray(` ${tr('new.api.prereq.1')}`));
|
|
257
|
-
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'));
|
|
258
191
|
} else {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
192
|
+
items.push(tr('new.api.prereq.1'));
|
|
193
|
+
items.push(tr('new.api.prereq.2'));
|
|
194
|
+
items.push(tr('new.api.prereq.3'));
|
|
262
195
|
}
|
|
263
196
|
}
|
|
264
|
-
|
|
197
|
+
|
|
198
|
+
if (items.length > 0) {
|
|
199
|
+
ui.note(items.map((i) => `• ${i}`).join('\n'), tr('new.prereq.title'));
|
|
200
|
+
}
|
|
265
201
|
}
|
|
266
202
|
|
|
267
203
|
function printSummary(tr, answers) {
|
|
@@ -270,25 +206,18 @@ function printSummary(tr, answers) {
|
|
|
270
206
|
: tr('new.firebase.confirm.none');
|
|
271
207
|
|
|
272
208
|
const backendLabel = { firebase: '🔥 Firebase', supabase: '🟢 Supabase', api: '🔗 API REST' }[answers.backend] || answers.backend;
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
if (answers.
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
console.log(` ${kleur.dim('Supabase URL:')} ${kleur.white(answers.supabaseUrl)}`);
|
|
286
|
-
}
|
|
287
|
-
if (answers.apiBaseUrl) {
|
|
288
|
-
console.log(` ${kleur.dim('API URL:')} ${kleur.white(answers.apiBaseUrl)}`);
|
|
289
|
-
}
|
|
290
|
-
console.log(` ${kleur.dim(tr('new.firebase.confirm.modules') + ':')} ${kleur.white(modules)}`);
|
|
291
|
-
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')));
|
|
292
221
|
}
|
|
293
222
|
|
|
294
223
|
const STEP_LABELS = {
|
|
@@ -377,38 +306,35 @@ function stepProgress(key, lang) {
|
|
|
377
306
|
function printCreateFromScratchStatus(result, tr) {
|
|
378
307
|
if (result.sha1Skipped) {
|
|
379
308
|
const err = (result.sha1Error || '').replace(/\s+/g, ' ').slice(0, 120);
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
}
|
|
385
|
-
console.log(kleur.yellow(` ${tr('new.sha1.addManually')}`));
|
|
386
|
-
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}`);
|
|
387
314
|
} else {
|
|
388
|
-
|
|
315
|
+
ui.log.success(tr('new.sha1.added'));
|
|
389
316
|
}
|
|
390
317
|
|
|
391
318
|
if (result.firestoreCreated) {
|
|
392
|
-
|
|
319
|
+
ui.log.success(tr('new.firestore.created'));
|
|
393
320
|
} else {
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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)}`);
|
|
398
325
|
}
|
|
399
326
|
|
|
400
327
|
if (result.storageCreated) {
|
|
401
|
-
|
|
328
|
+
ui.log.success(tr('new.storage.created'));
|
|
402
329
|
} else {
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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)}`);
|
|
407
334
|
}
|
|
408
335
|
}
|
|
409
336
|
|
|
410
337
|
function printSuccessCard(tr, answers, targetDir) {
|
|
411
|
-
const bar = kleur.gray(' ─────────────────────────────────────────────');
|
|
412
338
|
const folderName = path.basename(targetDir);
|
|
413
339
|
const consoleUrl = answers.backend === 'firebase' && answers.firebaseProjectId
|
|
414
340
|
? `https://console.firebase.google.com/project/${answers.firebaseProjectId}`
|
|
@@ -416,71 +342,62 @@ function printSuccessCard(tr, answers, targetDir) {
|
|
|
416
342
|
? 'https://supabase.com/dashboard'
|
|
417
343
|
: answers.apiBaseUrl || null;
|
|
418
344
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
console.log('');
|
|
425
|
-
console.log(` ${kleur.dim('1.')} ${kleur.dim(tr('new.success.step.cd'))}`);
|
|
426
|
-
console.log(` ${kleur.cyan(`cd ${folderName}`)}`);
|
|
427
|
-
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}`)}`);
|
|
428
350
|
let stepNum = 2;
|
|
429
351
|
if (answers.backend === 'firebase') {
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
352
|
+
lines.push('');
|
|
353
|
+
lines.push(`${kleur.dim(`${stepNum++}.`)} ${kleur.dim(tr('new.success.step.deploy'))}`);
|
|
354
|
+
lines.push(` ${kleur.cyan('kasy deploy')}`);
|
|
433
355
|
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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'))}`);
|
|
437
360
|
if (consoleUrl) {
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
361
|
+
lines.push('');
|
|
362
|
+
lines.push(`${kleur.dim(`${stepNum}.`)} ${kleur.dim(tr('new.success.step.console'))}`);
|
|
363
|
+
lines.push(` ${kleur.cyan(consoleUrl)}`);
|
|
441
364
|
}
|
|
442
|
-
|
|
443
|
-
console.log(
|
|
444
|
-
console.log('');
|
|
365
|
+
|
|
366
|
+
console.log(successBox(`🎉 ${tr('new.success.title')}`, lines.join('\n')));
|
|
445
367
|
}
|
|
446
368
|
|
|
447
369
|
function printStepResult(step, lang = 'pt') {
|
|
370
|
+
const label = stepLabel(step.name, lang);
|
|
448
371
|
if (step.skipped) {
|
|
449
|
-
|
|
450
|
-
console.log(` ${kleur.gray('⏭')} ${kleur.gray(label)} ${kleur.gray('(skipped)')}`);
|
|
372
|
+
ui.log.step(`${kleur.dim(label)} ${kleur.dim('(skipped)')}`);
|
|
451
373
|
return;
|
|
452
374
|
}
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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
|
+
}
|
|
458
381
|
}
|
|
459
382
|
|
|
460
383
|
function onCancel(tr) {
|
|
461
|
-
|
|
384
|
+
ui.cancel(tr('new.firebase.error.aborted'));
|
|
462
385
|
process.exit(0);
|
|
463
386
|
}
|
|
464
387
|
|
|
465
388
|
async function promptSupabaseManual(tr, cancel) {
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
message: tr('prompt.supabase.anonKey.enter'),
|
|
479
|
-
validate: (v) => (v && v.trim() ? true : tr('prompt.supabase.anonKey.required')),
|
|
480
|
-
},
|
|
481
|
-
],
|
|
482
|
-
{ onCancel: cancel }
|
|
483
|
-
);
|
|
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 };
|
|
484
401
|
}
|
|
485
402
|
|
|
486
403
|
// ── Module presets ────────────────────────────────────────────────────────────
|
|
@@ -534,6 +451,8 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
534
451
|
|
|
535
452
|
printBanner(tr);
|
|
536
453
|
|
|
454
|
+
ui.intro(kleur.bold(tr('new.subtitle2')));
|
|
455
|
+
|
|
537
456
|
// Whether an explicit target directory was provided by the user
|
|
538
457
|
const hasExplicitDir = directory && directory !== '.';
|
|
539
458
|
|
|
@@ -553,24 +472,19 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
553
472
|
// ── 3. Backend selection (support pre-selection via --backend flag) ─────
|
|
554
473
|
let backend = normalizeBackend(backendHint);
|
|
555
474
|
if (!backend) {
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
initial: 0,
|
|
567
|
-
},
|
|
568
|
-
{ onCancel: cancel }
|
|
569
|
-
);
|
|
570
|
-
backend = selectedBackend;
|
|
475
|
+
backend = await ui.select({
|
|
476
|
+
message: tr('new.q.backend'),
|
|
477
|
+
initialValue: 'firebase',
|
|
478
|
+
options: [
|
|
479
|
+
{ value: 'firebase', label: '🔥 Firebase', hint: tr('new.q.backend.firebase.desc') },
|
|
480
|
+
{ value: 'supabase', label: '🟢 Supabase', hint: tr('new.q.backend.supabase.desc') },
|
|
481
|
+
{ value: 'api', label: '🔗 API REST', hint: tr('new.q.backend.api.desc') },
|
|
482
|
+
],
|
|
483
|
+
onCancel: cancel,
|
|
484
|
+
});
|
|
571
485
|
} else {
|
|
572
486
|
const backendLabel = { firebase: '🔥 Firebase', supabase: '🟢 Supabase', api: '🔗 API REST' }[backend] || backend;
|
|
573
|
-
|
|
487
|
+
ui.log.info(`Backend: ${kleur.white(backendLabel)}`);
|
|
574
488
|
}
|
|
575
489
|
|
|
576
490
|
// ── 3b. Backend tool checks (required — blocks if missing) ───────────────
|
|
@@ -579,7 +493,7 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
579
493
|
let backendCheckResults = [];
|
|
580
494
|
if (backendChecks.length > 0) {
|
|
581
495
|
if (backend === 'supabase' || backend === 'api') {
|
|
582
|
-
|
|
496
|
+
ui.log.info(tr('new.checks.firebaseForPush'));
|
|
583
497
|
}
|
|
584
498
|
const backendLabel = tr('setup.checks.backend', { backend }) || ` Checking ${backend} tools…`;
|
|
585
499
|
backendCheckResults = await runChecks(backendChecks, backendLabel, {
|
|
@@ -589,53 +503,111 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
589
503
|
doneLabel: tr('setup.checks.backend.done', { backend }) || `${backend} tools ready`,
|
|
590
504
|
});
|
|
591
505
|
if (hasRequiredFailures(backendCheckResults)) {
|
|
592
|
-
|
|
593
|
-
console.log(kleur.dim(`\n ${tr('new.checks.installFirebase')}`));
|
|
506
|
+
const installSteps = [tr('new.checks.installFirebase')];
|
|
594
507
|
if (backend === 'supabase') {
|
|
595
508
|
const platform = process.platform === 'darwin' ? 'darwin' : process.platform === 'win32' ? 'win32' : 'linux';
|
|
596
509
|
const supabaseKey = `new.checks.installSupabase.${platform}`;
|
|
597
|
-
|
|
510
|
+
installSteps.push(tr(supabaseKey) || tr('new.checks.installSupabase'));
|
|
598
511
|
}
|
|
599
|
-
|
|
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'));
|
|
600
515
|
process.exit(1);
|
|
601
516
|
}
|
|
602
517
|
}
|
|
603
518
|
|
|
519
|
+
// ── App identity: name + bundle ID — first things first ────────────────────
|
|
520
|
+
let core;
|
|
521
|
+
if (yes) {
|
|
522
|
+
if (!hasExplicitDir) {
|
|
523
|
+
ui.log.error('--yes requires an app name: kasy new MyApp --yes');
|
|
524
|
+
ui.cancel(tr('new.firebase.error.aborted'));
|
|
525
|
+
process.exit(1);
|
|
526
|
+
}
|
|
527
|
+
const appName = path.basename(targetDir);
|
|
528
|
+
const slug = appName
|
|
529
|
+
.normalize('NFD')
|
|
530
|
+
.replace(/[̀-ͯ]/g, '')
|
|
531
|
+
.toLowerCase()
|
|
532
|
+
.replace(/[^a-z0-9]/g, '');
|
|
533
|
+
const bundleId = (slug && !/^\d/.test(slug)) ? `com.${slug}.app` : 'com.example.app';
|
|
534
|
+
core = { appName, bundleId };
|
|
535
|
+
ui.log.info(`App: ${kleur.white(appName)}`);
|
|
536
|
+
ui.log.info(`Bundle: ${kleur.white(bundleId)}`);
|
|
537
|
+
} else {
|
|
538
|
+
const appName = await ui.text({
|
|
539
|
+
message: tr('new.firebase.q.appName'),
|
|
540
|
+
placeholder: tr('new.firebase.q.appName.hint'),
|
|
541
|
+
initialValue: hasExplicitDir ? path.basename(targetDir) : '',
|
|
542
|
+
validate: (v) => (v && v.trim() ? undefined : tr('new.firebase.q.appName.required')),
|
|
543
|
+
onCancel: cancel,
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
const defaultBundleId = (() => {
|
|
547
|
+
const name = (appName || '').trim();
|
|
548
|
+
if (!name) return 'com.example.app';
|
|
549
|
+
const slug = name
|
|
550
|
+
.normalize('NFD')
|
|
551
|
+
.replace(/[̀-ͯ]/g, '')
|
|
552
|
+
.toLowerCase()
|
|
553
|
+
.replace(/[^a-z0-9]/g, '');
|
|
554
|
+
if (!slug || /^\d/.test(slug)) return 'com.example.app';
|
|
555
|
+
return `com.${slug}.app`;
|
|
556
|
+
})();
|
|
557
|
+
|
|
558
|
+
const bundleId = await ui.text({
|
|
559
|
+
message: tr('new.firebase.q.bundleId'),
|
|
560
|
+
placeholder: tr('new.firebase.q.bundleId.hint'),
|
|
561
|
+
initialValue: defaultBundleId,
|
|
562
|
+
validate: (v) => {
|
|
563
|
+
if (!v || !v.trim()) return tr('new.firebase.q.bundleId.required');
|
|
564
|
+
return /^[a-zA-Z][\w]*(\.[a-zA-Z][\w]*)+$/.test(v.trim())
|
|
565
|
+
? undefined
|
|
566
|
+
: tr('new.firebase.q.bundleId.invalid');
|
|
567
|
+
},
|
|
568
|
+
onCancel: cancel,
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
core = { appName, bundleId };
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Resolve targetDir now that we have the app name
|
|
575
|
+
if (!targetDir) {
|
|
576
|
+
const folderName = toPackageName(core.appName.trim());
|
|
577
|
+
targetDir = path.resolve(process.cwd(), folderName);
|
|
578
|
+
if (await fs.pathExists(targetDir)) {
|
|
579
|
+
const contents = await fs.readdir(targetDir);
|
|
580
|
+
if (contents.length > 0) {
|
|
581
|
+
throw new Error(tr('new.firebase.error.dirNotEmpty', { path: targetDir }));
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
604
586
|
// ── Firebase setup mode (create vs existing) ────────────────────────────────
|
|
605
587
|
// Firebase backend: full setup. Supabase/API: Firebase only for push notifications (FCM).
|
|
606
588
|
let firebaseSetupMode = 'existing';
|
|
607
589
|
if (!yes) {
|
|
608
590
|
if (backend === 'firebase') {
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
initial: 0,
|
|
619
|
-
},
|
|
620
|
-
{ onCancel: cancel }
|
|
621
|
-
);
|
|
622
|
-
firebaseSetupMode = setupMode;
|
|
591
|
+
firebaseSetupMode = await ui.select({
|
|
592
|
+
message: tr('new.firebase.q.setupMode'),
|
|
593
|
+
initialValue: 'create',
|
|
594
|
+
options: [
|
|
595
|
+
{ value: 'create', label: tr('new.firebase.q.setupMode.create') },
|
|
596
|
+
{ value: 'existing', label: tr('new.firebase.q.setupMode.existing') },
|
|
597
|
+
],
|
|
598
|
+
onCancel: cancel,
|
|
599
|
+
});
|
|
623
600
|
} else if (backend === 'supabase' || backend === 'api') {
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
initial: 0,
|
|
635
|
-
},
|
|
636
|
-
{ onCancel: cancel }
|
|
637
|
-
);
|
|
638
|
-
firebaseSetupMode = setupMode;
|
|
601
|
+
ui.note(tr('new.firebase.q.setupMode.push.explain'));
|
|
602
|
+
firebaseSetupMode = await ui.select({
|
|
603
|
+
message: tr('new.firebase.q.setupMode.push'),
|
|
604
|
+
initialValue: 'create',
|
|
605
|
+
options: [
|
|
606
|
+
{ value: 'create', label: tr('new.firebase.q.setupMode.create') },
|
|
607
|
+
{ value: 'existing', label: tr('new.firebase.q.setupMode.existing') },
|
|
608
|
+
],
|
|
609
|
+
onCancel: cancel,
|
|
610
|
+
});
|
|
639
611
|
}
|
|
640
612
|
}
|
|
641
613
|
|
|
@@ -643,184 +615,108 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
643
615
|
// Asked after app name + setup context are clear, so the user understands what it controls.
|
|
644
616
|
let isQuick = yes; // --yes implies Quick mode
|
|
645
617
|
if (!yes) {
|
|
646
|
-
const
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
initial: 0,
|
|
656
|
-
},
|
|
657
|
-
{ onCancel: cancel }
|
|
658
|
-
);
|
|
618
|
+
const wizardMode = await ui.select({
|
|
619
|
+
message: tr('new.q.mode'),
|
|
620
|
+
initialValue: 'quick',
|
|
621
|
+
options: [
|
|
622
|
+
{ value: 'quick', label: tr('new.q.mode.quick') },
|
|
623
|
+
{ value: 'advanced', label: tr('new.q.mode.advanced') },
|
|
624
|
+
],
|
|
625
|
+
onCancel: cancel,
|
|
626
|
+
});
|
|
659
627
|
isQuick = wizardMode === 'quick';
|
|
660
628
|
}
|
|
661
629
|
|
|
662
630
|
// ── Backend-specific prerequisites (dynamic: only show what's not yet verified) ─
|
|
663
631
|
printPrerequisites(tr, backend, firebaseSetupMode, backendCheckResults);
|
|
664
632
|
|
|
665
|
-
// ── Firebase
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
message: tr('new.firebase.q.
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
// ── Core questions (appName, bundleId) ────────────────────────────────────
|
|
686
|
-
const coreQuestions = [
|
|
687
|
-
{
|
|
688
|
-
type: 'text',
|
|
689
|
-
name: 'appName',
|
|
690
|
-
message: tr('new.firebase.q.appName'),
|
|
691
|
-
hint: tr('new.firebase.q.appName.hint'),
|
|
692
|
-
initial: hasExplicitDir ? path.basename(targetDir) : '',
|
|
693
|
-
validate: (v) => (v && v.trim() ? true : tr('new.firebase.q.appName.required')),
|
|
694
|
-
},
|
|
695
|
-
{
|
|
696
|
-
type: 'text',
|
|
697
|
-
name: 'bundleId',
|
|
698
|
-
message: tr('new.firebase.q.bundleId'),
|
|
699
|
-
hint: tr('new.firebase.q.bundleId.hint'),
|
|
700
|
-
initial: (prev, values) => {
|
|
701
|
-
const name = (values?.appName || prev || '').trim();
|
|
702
|
-
if (!name) return 'com.example.app';
|
|
703
|
-
const slug = name
|
|
704
|
-
.normalize('NFD')
|
|
705
|
-
.replace(/[\u0300-\u036f]/g, '')
|
|
706
|
-
.toLowerCase()
|
|
707
|
-
.replace(/[^a-z0-9]/g, '');
|
|
708
|
-
if (!slug || /^\d/.test(slug)) return 'com.example.app';
|
|
709
|
-
return `com.${slug}.app`;
|
|
710
|
-
},
|
|
711
|
-
validate: (v) => {
|
|
712
|
-
if (!v || !v.trim()) return tr('new.firebase.q.bundleId.required');
|
|
713
|
-
return /^[a-zA-Z][\w]*(\.[a-zA-Z][\w]*)+$/.test(v.trim())
|
|
714
|
-
? true
|
|
715
|
-
: tr('new.firebase.q.bundleId.invalid');
|
|
716
|
-
},
|
|
717
|
-
},
|
|
718
|
-
];
|
|
719
|
-
const needFirebaseProjectIdNow =
|
|
720
|
-
(backend === 'firebase' && firebaseSetupMode === 'existing') ||
|
|
721
|
-
((backend === 'supabase' || backend === 'api') && firebaseSetupMode === 'existing');
|
|
722
|
-
if (needFirebaseProjectIdNow) {
|
|
723
|
-
coreQuestions.push({
|
|
724
|
-
type: 'text',
|
|
725
|
-
name: 'firebaseProjectId',
|
|
726
|
-
message: tr('new.firebase.q.projectId'),
|
|
727
|
-
hint: tr('new.firebase.q.projectId.hint') + (backend !== 'firebase' ? ' (FCM + Remote Config)' : ''),
|
|
728
|
-
validate: (v) => {
|
|
729
|
-
if (!v || !v.trim()) return tr('new.firebase.q.projectId.required');
|
|
730
|
-
const id = v.trim();
|
|
731
|
-
if (id.length < 6 || id.length > 30) return 'ID deve ter entre 6 e 30 caracteres (ex: meu-app-123)';
|
|
732
|
-
if (!/^[a-z][a-z0-9-]*[a-z0-9]$/.test(id)) return 'ID inválido: use letras minúsculas, números e hífens, começando com letra (ex: meu-app-123)';
|
|
733
|
-
return true;
|
|
734
|
-
},
|
|
735
|
-
});
|
|
736
|
-
}
|
|
737
|
-
let core;
|
|
738
|
-
if (yes) {
|
|
739
|
-
if (!hasExplicitDir) {
|
|
740
|
-
console.error(kleur.red(`\n ✗ --yes requires an app name: kasy new MyApp --yes\n`));
|
|
741
|
-
process.exit(1);
|
|
633
|
+
// ── Firebase project ID (if using an existing project) ──────────────────────
|
|
634
|
+
if (!yes) {
|
|
635
|
+
const needFirebaseProjectId =
|
|
636
|
+
(backend === 'firebase' && firebaseSetupMode === 'existing') ||
|
|
637
|
+
((backend === 'supabase' || backend === 'api') && firebaseSetupMode === 'existing');
|
|
638
|
+
if (needFirebaseProjectId) {
|
|
639
|
+
const firebaseProjectId = await ui.text({
|
|
640
|
+
message: tr('new.firebase.q.projectId'),
|
|
641
|
+
placeholder: tr('new.firebase.q.projectId.hint') + (backend !== 'firebase' ? ' (FCM + Remote Config)' : ''),
|
|
642
|
+
validate: (v) => {
|
|
643
|
+
if (!v || !v.trim()) return tr('new.firebase.q.projectId.required');
|
|
644
|
+
const id = v.trim();
|
|
645
|
+
if (id.length < 6 || id.length > 30) return 'ID deve ter entre 6 e 30 caracteres (ex: meu-app-123)';
|
|
646
|
+
if (!/^[a-z][a-z0-9-]*[a-z0-9]$/.test(id)) return 'ID inválido: use letras minúsculas, números e hífens, começando com letra (ex: meu-app-123)';
|
|
647
|
+
return undefined;
|
|
648
|
+
},
|
|
649
|
+
onCancel: cancel,
|
|
650
|
+
});
|
|
651
|
+
core.firebaseProjectId = firebaseProjectId;
|
|
742
652
|
}
|
|
743
|
-
|
|
744
|
-
const slug = appName
|
|
745
|
-
.normalize('NFD')
|
|
746
|
-
.replace(/[\u0300-\u036f]/g, '')
|
|
747
|
-
.toLowerCase()
|
|
748
|
-
.replace(/[^a-z0-9]/g, '');
|
|
749
|
-
const bundleId = (slug && !/^\d/.test(slug)) ? `com.${slug}.app` : 'com.example.app';
|
|
653
|
+
} else {
|
|
750
654
|
let firebaseProjectId = projectHint?.trim() || '';
|
|
751
655
|
if (!firebaseProjectId && backend === 'firebase') {
|
|
752
|
-
const
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
validate: (v) => (v && v.trim() ? true : tr('new.firebase.q.projectId.required')),
|
|
759
|
-
},
|
|
760
|
-
{ onCancel: cancel }
|
|
761
|
-
);
|
|
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
|
+
});
|
|
762
662
|
firebaseProjectId = pid?.trim() || '';
|
|
763
663
|
}
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
}
|
|
769
|
-
|
|
664
|
+
if (firebaseProjectId) {
|
|
665
|
+
core.firebaseProjectId = firebaseProjectId;
|
|
666
|
+
ui.log.info(`Project: ${kleur.white(firebaseProjectId)}`);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// ── Firebase region — Quick mode uses default (us-central1) ──────────
|
|
671
|
+
let firebaseRegion = 'us-central1';
|
|
672
|
+
if (backend === 'firebase' && !isQuick) {
|
|
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
|
+
});
|
|
683
|
+
firebaseRegion = region || 'us-central1';
|
|
770
684
|
}
|
|
771
685
|
|
|
772
686
|
// ── Firebase: create from scratch (when selected) ─────────────────────────
|
|
773
687
|
let firebaseIncludeWeb = true;
|
|
774
688
|
if (backend === 'firebase' && firebaseSetupMode === 'create') {
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
initial: true,
|
|
781
|
-
},
|
|
782
|
-
{ onCancel: cancel }
|
|
783
|
-
);
|
|
784
|
-
firebaseIncludeWeb = includeWeb !== false;
|
|
689
|
+
firebaseIncludeWeb = await ui.confirm({
|
|
690
|
+
message: tr('new.firebase.create.includeWeb'),
|
|
691
|
+
initialValue: true,
|
|
692
|
+
onCancel: cancel,
|
|
693
|
+
});
|
|
785
694
|
const gcloudCheck = await checkGcloudAuth();
|
|
786
695
|
if (!gcloudCheck.ok) {
|
|
787
|
-
|
|
696
|
+
ui.log.error(tr('new.firebase.create.gcloudRequired'));
|
|
788
697
|
if (gcloudCheck.missing === 'gcloud') {
|
|
789
698
|
const instructions = getGcloudInstallInstructions();
|
|
790
|
-
|
|
791
|
-
if (instructions.install) {
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
console.log(kleur.gray(` ${instructions.hint}`));
|
|
797
|
-
}
|
|
798
|
-
console.log(kleur.white(` ${tr('new.firebase.create.installAfter')}:`));
|
|
799
|
-
console.log(kleur.cyan(` ${instructions.after}`));
|
|
800
|
-
console.log(kleur.gray(` ${tr('new.firebase.create.installUrl')}: ${instructions.url}`));
|
|
801
|
-
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'));
|
|
802
705
|
} else {
|
|
803
|
-
|
|
804
|
-
console.log('');
|
|
706
|
+
ui.note(tr('new.firebase.create.authCommand'));
|
|
805
707
|
}
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
validate: (v) => (v && v.trim() ? true : tr('new.firebase.q.projectId.required')),
|
|
814
|
-
},
|
|
815
|
-
{ onCancel: cancel }
|
|
816
|
-
);
|
|
817
|
-
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
|
+
});
|
|
818
715
|
} else {
|
|
819
716
|
const selectedOrgId = await promptOrganizationIfNeeded(tr, cancel);
|
|
820
717
|
let selectedBillingId = await promptBillingAccountIfNeeded(tr, cancel);
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
const ps1 = makeProgressSpinner();
|
|
718
|
+
ui.log.info(`${tr('new.firebase.create.estimatedTime')}\n${tr('new.internet.warning')}`);
|
|
719
|
+
const ps1 = ui.makeStepper();
|
|
824
720
|
ps1.next(tr('new.firebase.create.creating'));
|
|
825
721
|
const setupResult = await setupFromScratch(core.appName.trim(), core.bundleId.trim(), {
|
|
826
722
|
includeWeb: firebaseIncludeWeb,
|
|
@@ -837,15 +733,31 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
837
733
|
ps1.next(stepProgress('storage', language));
|
|
838
734
|
} else if (key === 'auth-providers-warn') {
|
|
839
735
|
ps1.stop();
|
|
840
|
-
|
|
841
|
-
console.log(kleur.cyan(` ${data?.url || ''}`));
|
|
736
|
+
ui.log.warn(`${tr('new.firebase.interactive.authWarn')}\n${kleur.cyan(data?.url || '')}`);
|
|
842
737
|
}
|
|
843
738
|
},
|
|
844
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
|
+
};
|
|
845
757
|
if (setupResult.ok) {
|
|
846
758
|
ps1.succeed(tr('new.firebase.create.success'));
|
|
847
759
|
core.firebaseProjectId = setupResult.projectId;
|
|
848
|
-
|
|
760
|
+
ui.log.info(`Project ID: ${core.firebaseProjectId}`);
|
|
849
761
|
printCreateFromScratchStatus(setupResult, tr);
|
|
850
762
|
const authUrl = `https://console.firebase.google.com/project/${core.firebaseProjectId}/authentication/providers`;
|
|
851
763
|
if (setupResult.authEnabled && !setupResult.googleSignInSkipped) {
|
|
@@ -858,50 +770,33 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
858
770
|
const readyKey = setupResult.googleSignInSkipped
|
|
859
771
|
? 'new.firebase.create.beforeContinue.ready'
|
|
860
772
|
: 'new.firebase.create.beforeContinue.ready.noAuth';
|
|
861
|
-
|
|
862
|
-
console.log(kleur.gray(` ${tr(step1Key)}`));
|
|
863
|
-
console.log(kleur.cyan(` ${authUrl}`));
|
|
773
|
+
showBeforeContinue(step1Key, authUrl);
|
|
864
774
|
openUrl(authUrl);
|
|
865
|
-
|
|
866
|
-
{
|
|
867
|
-
type: 'confirm',
|
|
868
|
-
name: 'ready',
|
|
869
|
-
message: tr(readyKey),
|
|
870
|
-
initial: true,
|
|
871
|
-
},
|
|
872
|
-
{ onCancel: cancel }
|
|
873
|
-
);
|
|
874
|
-
if (!ready) {
|
|
875
|
-
console.log(kleur.gray(` ${tr('prompt.cancelled')}`));
|
|
876
|
-
process.exit(0);
|
|
877
|
-
}
|
|
775
|
+
await askReady(readyKey);
|
|
878
776
|
}
|
|
879
777
|
|
|
880
778
|
} else {
|
|
881
779
|
ps1.fail(tr('new.firebase.create.failed'));
|
|
882
780
|
let lastResult = setupResult;
|
|
883
781
|
while (lastResult.billingFailed && lastResult.projectId) {
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
const { retry } = await prompts(
|
|
889
|
-
{
|
|
890
|
-
type: 'confirm',
|
|
891
|
-
name: 'retry',
|
|
892
|
-
message: tr('new.firebase.create.billingRetry.ready'),
|
|
893
|
-
initial: true,
|
|
894
|
-
},
|
|
895
|
-
{ 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')
|
|
896
786
|
);
|
|
787
|
+
const retry = await ui.confirm({
|
|
788
|
+
message: tr('new.firebase.create.billingRetry.ready'),
|
|
789
|
+
initialValue: true,
|
|
790
|
+
onCancel: cancel,
|
|
791
|
+
});
|
|
897
792
|
if (!retry) {
|
|
898
|
-
|
|
899
|
-
|
|
793
|
+
ui.log.message(tr('new.firebase.create.billingRetry.exit'));
|
|
794
|
+
ui.log.info(tr('new.firebase.create.useExistingHint', { id: lastResult.projectId }));
|
|
900
795
|
process.exit(0);
|
|
901
796
|
}
|
|
902
|
-
|
|
797
|
+
ui.log.message(tr('new.firebase.create.billingRetry.retrying'));
|
|
903
798
|
selectedBillingId = await promptBillingAccountIfNeeded(tr, cancel);
|
|
904
|
-
const ps2 =
|
|
799
|
+
const ps2 = ui.makeStepper();
|
|
905
800
|
ps2.next(tr('new.firebase.create.creating'));
|
|
906
801
|
lastResult = await setupFromScratch(core.appName.trim(), core.bundleId.trim(), {
|
|
907
802
|
includeWeb: firebaseIncludeWeb,
|
|
@@ -923,7 +818,7 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
923
818
|
if (lastResult.ok) {
|
|
924
819
|
ps2.succeed(tr('new.firebase.create.success'));
|
|
925
820
|
core.firebaseProjectId = lastResult.projectId;
|
|
926
|
-
|
|
821
|
+
ui.log.info(`Project ID: ${core.firebaseProjectId}`);
|
|
927
822
|
printCreateFromScratchStatus(lastResult, tr);
|
|
928
823
|
const authUrl = `https://console.firebase.google.com/project/${core.firebaseProjectId}/authentication/providers`;
|
|
929
824
|
if (lastResult.authEnabled && !lastResult.googleSignInSkipped) {
|
|
@@ -935,47 +830,32 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
935
830
|
const lastReadyKey = lastResult.googleSignInSkipped
|
|
936
831
|
? 'new.firebase.create.beforeContinue.ready'
|
|
937
832
|
: 'new.firebase.create.beforeContinue.ready.noAuth';
|
|
938
|
-
|
|
939
|
-
console.log(kleur.gray(` ${tr(step1Key)}`));
|
|
940
|
-
console.log(kleur.cyan(` ${authUrl}`));
|
|
833
|
+
showBeforeContinue(step1Key, authUrl);
|
|
941
834
|
openUrl(authUrl);
|
|
942
|
-
|
|
943
|
-
{
|
|
944
|
-
type: 'confirm',
|
|
945
|
-
name: 'ready',
|
|
946
|
-
message: tr(lastReadyKey),
|
|
947
|
-
initial: true,
|
|
948
|
-
},
|
|
949
|
-
{ onCancel: cancel }
|
|
950
|
-
);
|
|
951
|
-
if (!ready) {
|
|
952
|
-
console.log(kleur.gray(` ${tr('prompt.cancelled')}`));
|
|
953
|
-
process.exit(0);
|
|
954
|
-
}
|
|
835
|
+
await askReady(lastReadyKey);
|
|
955
836
|
}
|
|
956
837
|
|
|
957
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'));
|
|
958
843
|
}
|
|
959
844
|
}
|
|
960
845
|
if (!lastResult.ok && !lastResult.billingFailed) {
|
|
961
846
|
const isProjectQuota = /exceeded your allotted project quota|project quota/i.test(String(lastResult.error || ''));
|
|
962
847
|
const msg = isProjectQuota ? tr('new.firebase.create.projectQuotaExceeded') : `${tr('new.firebase.create.failed')}: ${lastResult.error}`;
|
|
963
|
-
|
|
848
|
+
ui.log.error(msg);
|
|
964
849
|
if (lastResult.projectId) {
|
|
965
850
|
core.firebaseProjectId = lastResult.projectId;
|
|
966
|
-
|
|
851
|
+
ui.log.message(tr('new.firebase.create.usingProjectId', { id: lastResult.projectId }));
|
|
967
852
|
} else {
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
validate: (v) => (v && v.trim() ? true : tr('new.firebase.q.projectId.required')),
|
|
975
|
-
},
|
|
976
|
-
{ onCancel: cancel }
|
|
977
|
-
);
|
|
978
|
-
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
|
+
});
|
|
979
859
|
}
|
|
980
860
|
}
|
|
981
861
|
}
|
|
@@ -986,42 +866,30 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
986
866
|
if ((backend === 'supabase' || backend === 'api') && firebaseSetupMode === 'create') {
|
|
987
867
|
const gcloudCheck = await checkGcloudAuth();
|
|
988
868
|
if (!gcloudCheck.ok) {
|
|
989
|
-
|
|
869
|
+
ui.log.error(tr('new.firebase.create.gcloudRequired'));
|
|
990
870
|
if (gcloudCheck.missing === 'gcloud') {
|
|
991
871
|
const instructions = getGcloudInstallInstructions();
|
|
992
|
-
|
|
993
|
-
if (instructions.install) {
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
console.log(kleur.gray(` ${instructions.hint}`));
|
|
999
|
-
}
|
|
1000
|
-
console.log(kleur.white(` ${tr('new.firebase.create.installAfter')}:`));
|
|
1001
|
-
console.log(kleur.cyan(` ${instructions.after}`));
|
|
1002
|
-
console.log(kleur.gray(` ${tr('new.firebase.create.installUrl')}: ${instructions.url}`));
|
|
1003
|
-
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'));
|
|
1004
878
|
} else {
|
|
1005
|
-
|
|
1006
|
-
console.log('');
|
|
879
|
+
ui.note(tr('new.firebase.create.authCommand'));
|
|
1007
880
|
}
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
validate: (v) => (v && v.trim() ? true : tr('new.firebase.q.projectId.required')),
|
|
1016
|
-
},
|
|
1017
|
-
{ onCancel: cancel }
|
|
1018
|
-
);
|
|
1019
|
-
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
|
+
});
|
|
1020
888
|
} else {
|
|
1021
889
|
const selectedOrgId = await promptOrganizationIfNeeded(tr, cancel);
|
|
1022
890
|
const selectedBillingId = await promptBillingAccountIfNeeded(tr, cancel);
|
|
1023
|
-
|
|
1024
|
-
const ps3 =
|
|
891
|
+
ui.log.info(tr('new.internet.warning'));
|
|
892
|
+
const ps3 = ui.makeStepper();
|
|
1025
893
|
ps3.next(tr('new.firebase.create.creatingPush'));
|
|
1026
894
|
const setupResult = await setupFromScratch(core.appName.trim(), core.bundleId.trim(), {
|
|
1027
895
|
includeWeb: true,
|
|
@@ -1042,26 +910,21 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1042
910
|
if (setupResult.ok) {
|
|
1043
911
|
ps3.succeed(tr('new.firebase.create.successPush'));
|
|
1044
912
|
core.firebaseProjectId = setupResult.projectId;
|
|
1045
|
-
|
|
913
|
+
ui.log.info(`Project ID: ${core.firebaseProjectId}`);
|
|
1046
914
|
printCreateFromScratchStatus(setupResult, tr);
|
|
1047
915
|
} else {
|
|
1048
916
|
ps3.fail(tr('new.firebase.create.failed'));
|
|
1049
|
-
|
|
917
|
+
ui.log.error(`${tr('new.firebase.create.failed')}: ${setupResult.error}`);
|
|
1050
918
|
if (setupResult.projectId) {
|
|
1051
919
|
core.firebaseProjectId = setupResult.projectId;
|
|
1052
|
-
|
|
920
|
+
ui.log.message(tr('new.firebase.create.usingProjectId', { id: setupResult.projectId }));
|
|
1053
921
|
} else {
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
validate: (v) => (v && v.trim() ? true : tr('new.firebase.q.projectId.required')),
|
|
1061
|
-
},
|
|
1062
|
-
{ onCancel: cancel }
|
|
1063
|
-
);
|
|
1064
|
-
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
|
+
});
|
|
1065
928
|
}
|
|
1066
929
|
}
|
|
1067
930
|
}
|
|
@@ -1075,91 +938,75 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1075
938
|
let supabaseExistingResult = null;
|
|
1076
939
|
|
|
1077
940
|
if (backend === 'supabase') {
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
{
|
|
1090
|
-
|
|
1091
|
-
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
|
+
};
|
|
1092
954
|
|
|
1093
955
|
if (supabaseCreate) {
|
|
1094
956
|
const loginCheck = await checkLoggedIn();
|
|
1095
|
-
if (!loginCheck.ok)
|
|
1096
|
-
console.log(kleur.yellow(` ${tr('new.supabase.loginRequired')}`));
|
|
1097
|
-
console.log(kleur.cyan(` ${tr('new.supabase.loginCommand')}`));
|
|
1098
|
-
console.log('');
|
|
1099
|
-
}
|
|
957
|
+
if (!loginCheck.ok) showLoginRequired();
|
|
1100
958
|
const orgsResult = await getOrgsList();
|
|
1101
959
|
if (!orgsResult.ok || !orgsResult.orgs?.length) {
|
|
1102
|
-
|
|
960
|
+
ui.log.error(tr('new.supabase.orgsRequired'));
|
|
1103
961
|
supabaseCreateResult = { ok: false, error: tr('new.supabase.orgsRequired') };
|
|
1104
962
|
} else {
|
|
1105
963
|
let orgId = orgsResult.orgs[0].id;
|
|
1106
964
|
if (orgsResult.orgs.length > 1) {
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
},
|
|
1114
|
-
{ onCancel: cancel }
|
|
1115
|
-
);
|
|
1116
|
-
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
|
+
});
|
|
1117
971
|
}
|
|
1118
972
|
let supabaseRegion = DEFAULT_SUPABASE_REGION;
|
|
1119
973
|
if (!isQuick) {
|
|
1120
|
-
const
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
],
|
|
1131
|
-
initial: 0,
|
|
1132
|
-
},
|
|
1133
|
-
{ onCancel: cancel }
|
|
1134
|
-
);
|
|
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
|
+
});
|
|
1135
984
|
supabaseRegion = region || DEFAULT_SUPABASE_REGION;
|
|
1136
985
|
}
|
|
1137
|
-
const
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
validate: (v) => (v && v.length >= 6 ? true : tr('new.supabase.q.dbPassword.required')),
|
|
1143
|
-
},
|
|
1144
|
-
{ onCancel: cancel }
|
|
1145
|
-
);
|
|
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
|
+
});
|
|
1146
991
|
supabaseDbPassword = dbPassword;
|
|
1147
|
-
|
|
1148
|
-
|
|
992
|
+
ui.log.info(tr('new.internet.warning'));
|
|
993
|
+
const createSpinner = ui.spinner();
|
|
994
|
+
createSpinner.start(tr('new.supabase.creating'));
|
|
1149
995
|
supabaseCreateResult = await createProjectAndGetKeys(
|
|
1150
996
|
core.appName.trim().replace(/\s+/g, '-').toLowerCase(),
|
|
1151
997
|
supabaseDbPassword,
|
|
1152
998
|
supabaseRegion,
|
|
1153
999
|
orgId
|
|
1154
1000
|
);
|
|
1001
|
+
createSpinner.stop(tr('new.supabase.creating'));
|
|
1155
1002
|
}
|
|
1156
1003
|
if (supabaseCreateResult.ok) {
|
|
1157
1004
|
core.supabaseUrl = supabaseCreateResult.supabaseUrl;
|
|
1158
1005
|
core.supabaseAnonKey = supabaseCreateResult.supabaseAnonKey;
|
|
1159
|
-
|
|
1006
|
+
ui.log.success(tr('new.supabase.created'));
|
|
1160
1007
|
} else {
|
|
1161
|
-
|
|
1162
|
-
|
|
1008
|
+
ui.log.error(`${tr('new.supabase.createFailed')}: ${supabaseCreateResult.error}`);
|
|
1009
|
+
ui.log.message(tr('new.supabase.loginHint'));
|
|
1163
1010
|
Object.assign(core, await promptSupabaseManual(tr, cancel));
|
|
1164
1011
|
supabaseCreate = false;
|
|
1165
1012
|
}
|
|
@@ -1167,54 +1014,46 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1167
1014
|
// Usar projeto Supabase existente: org → projeto → keys → senha (mesmo fluxo de setup, sem criar)
|
|
1168
1015
|
const loginCheck = await checkLoggedIn();
|
|
1169
1016
|
if (!loginCheck.ok) {
|
|
1170
|
-
|
|
1171
|
-
console.log(kleur.cyan(` ${tr('new.supabase.loginCommand')}`));
|
|
1172
|
-
console.log('');
|
|
1017
|
+
showLoginRequired();
|
|
1173
1018
|
Object.assign(core, await promptSupabaseManual(tr, cancel));
|
|
1174
1019
|
} else {
|
|
1175
1020
|
const orgsResult = await getOrgsList();
|
|
1176
1021
|
if (!orgsResult.ok || !orgsResult.orgs?.length) {
|
|
1177
|
-
|
|
1022
|
+
ui.log.error(tr('new.supabase.orgsRequired'));
|
|
1178
1023
|
Object.assign(core, await promptSupabaseManual(tr, cancel));
|
|
1179
1024
|
} else {
|
|
1180
1025
|
let orgId = orgsResult.orgs[0].id;
|
|
1181
1026
|
if (orgsResult.orgs.length > 1) {
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
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
|
+
});
|
|
1187
1033
|
}
|
|
1188
1034
|
const projectsResult = await getProjectsByOrg(orgId);
|
|
1189
1035
|
if (!projectsResult.ok || !projectsResult.projects?.length) {
|
|
1190
|
-
|
|
1036
|
+
ui.log.warn(tr('new.supabase.projectsRequired'));
|
|
1191
1037
|
Object.assign(core, await promptSupabaseManual(tr, cancel));
|
|
1192
1038
|
} else {
|
|
1193
|
-
const
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
},
|
|
1200
|
-
{ onCancel: cancel }
|
|
1201
|
-
);
|
|
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
|
+
});
|
|
1202
1045
|
const keysResult = await getProjectKeys(selectedProjectRef);
|
|
1203
1046
|
if (!keysResult.ok) {
|
|
1204
|
-
|
|
1047
|
+
ui.log.error(keysResult.error);
|
|
1205
1048
|
Object.assign(core, await promptSupabaseManual(tr, cancel));
|
|
1206
1049
|
} else {
|
|
1207
1050
|
core.supabaseUrl = keysResult.supabaseUrl;
|
|
1208
1051
|
core.supabaseAnonKey = keysResult.supabaseAnonKey;
|
|
1209
|
-
const
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
validate: (v) => (v && v.length >= 6 ? true : tr('new.supabase.q.dbPassword.required')),
|
|
1215
|
-
},
|
|
1216
|
-
{ onCancel: cancel }
|
|
1217
|
-
);
|
|
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
|
+
});
|
|
1218
1057
|
supabaseExistingResult = {
|
|
1219
1058
|
ok: true,
|
|
1220
1059
|
projectRef: selectedProjectRef,
|
|
@@ -1222,7 +1061,7 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1222
1061
|
supabaseAnonKey: keysResult.supabaseAnonKey,
|
|
1223
1062
|
dbPassword,
|
|
1224
1063
|
};
|
|
1225
|
-
|
|
1064
|
+
ui.log.success(tr('new.supabase.existingLinked'));
|
|
1226
1065
|
}
|
|
1227
1066
|
}
|
|
1228
1067
|
}
|
|
@@ -1238,35 +1077,17 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1238
1077
|
let googleIosClientId = '';
|
|
1239
1078
|
|
|
1240
1079
|
if (backend === 'api') {
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
initial: 'https://api.example.com',
|
|
1248
|
-
},
|
|
1249
|
-
{ onCancel: cancel }
|
|
1250
|
-
);
|
|
1251
|
-
Object.assign(core, api);
|
|
1252
|
-
}
|
|
1253
|
-
|
|
1254
|
-
// Resolve targetDir now that we have the app name
|
|
1255
|
-
if (!targetDir) {
|
|
1256
|
-
const folderName = toPackageName(core.appName.trim());
|
|
1257
|
-
targetDir = path.resolve(process.cwd(), folderName);
|
|
1258
|
-
// Guard: derived dir must not already exist
|
|
1259
|
-
if (await fs.pathExists(targetDir)) {
|
|
1260
|
-
const contents = await fs.readdir(targetDir);
|
|
1261
|
-
if (contents.length > 0) {
|
|
1262
|
-
throw new Error(tr('new.firebase.error.dirNotEmpty', { path: targetDir }));
|
|
1263
|
-
}
|
|
1264
|
-
}
|
|
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
|
+
});
|
|
1265
1086
|
}
|
|
1266
1087
|
|
|
1267
1088
|
// ── Firebase existing project: enable APIs + create Firestore/Storage ───
|
|
1268
1089
|
if (backend === 'firebase' && firebaseSetupMode === 'existing' && core.firebaseProjectId) {
|
|
1269
|
-
const ps4 =
|
|
1090
|
+
const ps4 = ui.makeStepper();
|
|
1270
1091
|
ps4.next(stepProgress('enable-apis', language));
|
|
1271
1092
|
const existingSetup = await setupExistingProject(core.firebaseProjectId, {
|
|
1272
1093
|
onProgress: (key, data) => {
|
|
@@ -1274,19 +1095,17 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1274
1095
|
ps4.next(stepProgress('enable-apis', language));
|
|
1275
1096
|
} else if (key === 'enable-apis-warn') {
|
|
1276
1097
|
ps4.warn(`${tr('new.firebase.create.failed')}: APIs`);
|
|
1277
|
-
|
|
1098
|
+
ui.log.warn(`${tr('new.firebase.existing.apisFailed')} ${(data?.error || '').slice(0, 80)}`);
|
|
1278
1099
|
} else if (key === 'firestore') {
|
|
1279
1100
|
ps4.next(stepProgress('firestore', language));
|
|
1280
1101
|
} else if (key === 'storage') {
|
|
1281
1102
|
ps4.next(stepProgress('storage', language));
|
|
1282
1103
|
} else if (key === 'auth-providers-warn') {
|
|
1283
1104
|
ps4.stop();
|
|
1284
|
-
|
|
1285
|
-
console.log(kleur.cyan(` ${data?.url || ''}`));
|
|
1105
|
+
ui.log.warn(`${tr('new.firebase.interactive.authWarn')}\n${kleur.cyan(data?.url || '')}`);
|
|
1286
1106
|
} else if (key === 'auth-google-warn') {
|
|
1287
1107
|
ps4.stop();
|
|
1288
|
-
|
|
1289
|
-
console.log(kleur.cyan(` ${data?.url || ''}`));
|
|
1108
|
+
ui.log.warn(`${tr('new.firebase.existing.googleSignInManual')}\n${kleur.cyan(data?.url || '')}`);
|
|
1290
1109
|
}
|
|
1291
1110
|
},
|
|
1292
1111
|
});
|
|
@@ -1294,17 +1113,15 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1294
1113
|
ps4.succeed(stepLabel('firestore', language));
|
|
1295
1114
|
} else if (existingSetup.firestoreError && !existingSetup.firestoreError.includes('ALREADY_EXISTS')) {
|
|
1296
1115
|
ps4.fail(`Firestore: ${(existingSetup.firestoreError || '').slice(0, 80)}`);
|
|
1297
|
-
|
|
1116
|
+
ui.log.message(kleur.cyan(existingSetup.firestoreUrl));
|
|
1298
1117
|
} else {
|
|
1299
1118
|
ps4.stop();
|
|
1300
1119
|
}
|
|
1301
1120
|
if (existingSetup.storageCreated) {
|
|
1302
|
-
|
|
1121
|
+
ui.log.success(stepLabel('storage', language));
|
|
1303
1122
|
} else if (existingSetup.storageError && !existingSetup.storageError.includes('ALREADY_EXISTS')) {
|
|
1304
|
-
|
|
1305
|
-
console.log(kleur.cyan(` ${existingSetup.storageUrl}`));
|
|
1123
|
+
ui.log.warn(`Storage: ${(existingSetup.storageError || '').slice(0, 80)}\n${kleur.cyan(existingSetup.storageUrl)}`);
|
|
1306
1124
|
}
|
|
1307
|
-
console.log('');
|
|
1308
1125
|
}
|
|
1309
1126
|
|
|
1310
1127
|
// ── Optional modules ────────────────────────────────────────────────────
|
|
@@ -1320,22 +1137,18 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1320
1137
|
modules = preselectedModules;
|
|
1321
1138
|
} else if (isQuick) {
|
|
1322
1139
|
// Quick mode: show preset picker (no multiselect).
|
|
1323
|
-
const
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
initial: 0,
|
|
1336
|
-
},
|
|
1337
|
-
{ onCancel: cancel }
|
|
1338
|
-
);
|
|
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
|
+
});
|
|
1339
1152
|
modules = MODULE_PRESETS[preset] || [];
|
|
1340
1153
|
} else {
|
|
1341
1154
|
// Advanced mode: full multiselect — built from catalog, filtered by audience + backend.
|
|
@@ -1353,7 +1166,9 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1353
1166
|
{ header: 'new.modules.header.ci', ids: ['ci'] },
|
|
1354
1167
|
];
|
|
1355
1168
|
|
|
1356
|
-
|
|
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 = [];
|
|
1357
1172
|
for (const group of groups) {
|
|
1358
1173
|
const inGroup = visibleFeatures.filter((f) => {
|
|
1359
1174
|
if (!group.ids.includes(f.id)) return false;
|
|
@@ -1361,29 +1176,24 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1361
1176
|
return true;
|
|
1362
1177
|
});
|
|
1363
1178
|
if (inGroup.length === 0) continue;
|
|
1364
|
-
|
|
1365
|
-
moduleChoices.push({ title: tr(group.header), value: `__header_${group.header}__`, disabled: true });
|
|
1366
|
-
}
|
|
1179
|
+
const groupLabel = group.header ? tr(group.header) : null;
|
|
1367
1180
|
for (const f of inGroup) {
|
|
1368
|
-
const
|
|
1369
|
-
|
|
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 });
|
|
1370
1186
|
}
|
|
1371
1187
|
}
|
|
1372
1188
|
|
|
1373
|
-
const
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
min: 0,
|
|
1382
|
-
warn: tr('prompt.multiselect.warnDisabled'),
|
|
1383
|
-
},
|
|
1384
|
-
{ onCancel: cancel }
|
|
1385
|
-
);
|
|
1386
|
-
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 || [];
|
|
1387
1197
|
}
|
|
1388
1198
|
|
|
1389
1199
|
if (backend === 'firebase' && firebaseSetupMode === 'create' && firebaseIncludeWeb) {
|
|
@@ -1394,57 +1204,41 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1394
1204
|
const moduleAnswers = {};
|
|
1395
1205
|
|
|
1396
1206
|
if (modules.includes('revenuecat')) {
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
{
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
hint: tr('new.firebase.q.paywall.hint'),
|
|
1416
|
-
choices: [
|
|
1417
|
-
{ title: 'Basic (list of plans)', value: 'basic' },
|
|
1418
|
-
{ title: 'With trial switch', value: 'withSwitch' },
|
|
1419
|
-
{ title: 'Row + comparison table', value: 'basicRow' },
|
|
1420
|
-
{ title: 'Minimal (benefits + CTA)', value: 'minimal' },
|
|
1421
|
-
],
|
|
1422
|
-
initial: 0,
|
|
1423
|
-
},
|
|
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)' },
|
|
1424
1225
|
],
|
|
1425
|
-
|
|
1426
|
-
);
|
|
1427
|
-
Object.assign(moduleAnswers, rc);
|
|
1226
|
+
onCancel: cancel,
|
|
1227
|
+
});
|
|
1428
1228
|
}
|
|
1429
1229
|
|
|
1430
1230
|
// RC web key — only in advanced mode (optional credential, can configure later).
|
|
1431
1231
|
if (!isQuick && modules.includes('revenuecat') && modules.includes('web')) {
|
|
1432
|
-
const rcWebKey = await
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
validate: (v) => {
|
|
1438
|
-
if (!v || !v.trim()) return true; // optional — blank is fine
|
|
1439
|
-
return /^rcb_/.test(v.trim()) ? true : tr('new.firebase.q.revenuecat.webKey.invalid');
|
|
1440
|
-
},
|
|
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');
|
|
1441
1237
|
},
|
|
1442
|
-
|
|
1443
|
-
);
|
|
1444
|
-
Object.assign(moduleAnswers, {
|
|
1445
|
-
revenuecatWeb: true,
|
|
1446
|
-
rcWebKey: (rcWebKey.rcWebKey || '').trim(),
|
|
1238
|
+
onCancel: cancel,
|
|
1447
1239
|
});
|
|
1240
|
+
moduleAnswers.revenuecatWeb = true;
|
|
1241
|
+
moduleAnswers.rcWebKey = (rcWebKey || '').trim();
|
|
1448
1242
|
} else if (modules.includes('revenuecat') && modules.includes('web')) {
|
|
1449
1243
|
// Quick mode: mark web billing as included but key will be configured later.
|
|
1450
1244
|
moduleAnswers.revenuecatWeb = true;
|
|
@@ -1453,76 +1247,52 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1453
1247
|
|
|
1454
1248
|
// Sentry DSN — already optional (blank = configure later). Skip in quick mode.
|
|
1455
1249
|
if (!isQuick && modules.includes('sentry')) {
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
return /^https?:\/\/[^@\s]+@[^/\s]+\/\d+$/.test(v.trim())
|
|
1464
|
-
? true
|
|
1465
|
-
: tr('new.firebase.q.sentry.dsn.invalid');
|
|
1466
|
-
},
|
|
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');
|
|
1467
1257
|
},
|
|
1468
|
-
|
|
1469
|
-
);
|
|
1470
|
-
Object.assign(moduleAnswers, s);
|
|
1258
|
+
onCancel: cancel,
|
|
1259
|
+
});
|
|
1471
1260
|
}
|
|
1472
1261
|
|
|
1473
1262
|
// Mixpanel token — already optional. Skip in quick mode.
|
|
1474
1263
|
if (!isQuick && modules.includes('analytics')) {
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
message: tr('new.firebase.q.mixpanel.token'),
|
|
1480
|
-
},
|
|
1481
|
-
{ onCancel: cancel }
|
|
1482
|
-
);
|
|
1483
|
-
Object.assign(moduleAnswers, mx);
|
|
1264
|
+
moduleAnswers.mixpanelToken = await ui.text({
|
|
1265
|
+
message: tr('new.firebase.q.mixpanel.token'),
|
|
1266
|
+
onCancel: cancel,
|
|
1267
|
+
});
|
|
1484
1268
|
}
|
|
1485
1269
|
|
|
1486
1270
|
// LLM Chat credentials — skip in quick mode, all fields optional.
|
|
1487
1271
|
if (!isQuick && modules.includes('llm_chat')) {
|
|
1488
|
-
const
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
hint: tr('new.q.llm_chat.configureNow.hint'),
|
|
1494
|
-
initial: true,
|
|
1495
|
-
},
|
|
1496
|
-
{ onCancel: cancel }
|
|
1497
|
-
);
|
|
1272
|
+
const configureLlmNow = await ui.confirm({
|
|
1273
|
+
message: tr('new.q.llm_chat.configureNow'),
|
|
1274
|
+
initialValue: true,
|
|
1275
|
+
onCancel: cancel,
|
|
1276
|
+
});
|
|
1498
1277
|
|
|
1499
1278
|
if (configureLlmNow) {
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
choices: [
|
|
1507
|
-
{ title: 'OpenAI (gpt-4o-mini)', value: 'openai' },
|
|
1508
|
-
{ title: 'Google Gemini (gemini-1.5-flash)', value: 'gemini' },
|
|
1509
|
-
],
|
|
1510
|
-
initial: 0,
|
|
1511
|
-
},
|
|
1512
|
-
{
|
|
1513
|
-
type: 'text',
|
|
1514
|
-
name: 'llmSystemPrompt',
|
|
1515
|
-
message: tr('add.prompt.llmSystemPrompt'),
|
|
1516
|
-
},
|
|
1517
|
-
{
|
|
1518
|
-
type: 'text',
|
|
1519
|
-
name: 'llmApiKey',
|
|
1520
|
-
message: tr('add.prompt.llmApiKey'),
|
|
1521
|
-
},
|
|
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)' },
|
|
1522
1285
|
],
|
|
1523
|
-
|
|
1524
|
-
);
|
|
1525
|
-
|
|
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
|
+
});
|
|
1526
1296
|
} else {
|
|
1527
1297
|
moduleAnswers.llmConfigureLater = true;
|
|
1528
1298
|
}
|
|
@@ -1532,70 +1302,48 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1532
1302
|
|
|
1533
1303
|
// Facebook — required credentials, always ask.
|
|
1534
1304
|
if (modules.includes('facebook')) {
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
name: 'fbToken',
|
|
1549
|
-
message: tr('new.firebase.q.facebook.token'),
|
|
1550
|
-
validate: (v) => (v && v.trim() ? true : tr('new.firebase.q.facebook.token.required')),
|
|
1551
|
-
},
|
|
1552
|
-
],
|
|
1553
|
-
{ onCancel: cancel }
|
|
1554
|
-
);
|
|
1555
|
-
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
|
+
});
|
|
1556
1318
|
}
|
|
1557
1319
|
|
|
1558
1320
|
// Server secrets (webhook, Meta Ads) — skip in quick mode, configure later via `kasy deploy`.
|
|
1559
1321
|
if (!isQuick && modules.includes('revenuecat') && (backend === 'supabase' || backend === 'firebase')) {
|
|
1560
|
-
const
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
hint: tr('new.firebase.q.secrets.configureNow.hint'),
|
|
1566
|
-
initial: true,
|
|
1567
|
-
},
|
|
1568
|
-
{ onCancel: cancel }
|
|
1569
|
-
);
|
|
1322
|
+
const configureSecretsNow = await ui.confirm({
|
|
1323
|
+
message: tr('new.firebase.q.secrets.configureNow'),
|
|
1324
|
+
initialValue: true,
|
|
1325
|
+
onCancel: cancel,
|
|
1326
|
+
});
|
|
1570
1327
|
|
|
1571
1328
|
if (configureSecretsNow) {
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
validate: (v) => {
|
|
1591
|
-
if (!v || !v.trim()) return true; // optional — blank is fine
|
|
1592
|
-
return /^\d+$/.test(v.trim()) ? true : tr('new.firebase.q.revenuecat.metaDataset.invalid');
|
|
1593
|
-
},
|
|
1594
|
-
},
|
|
1595
|
-
],
|
|
1596
|
-
{ onCancel: cancel }
|
|
1597
|
-
);
|
|
1598
|
-
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
|
+
});
|
|
1599
1347
|
} else {
|
|
1600
1348
|
moduleAnswers.secretsConfigureLater = true;
|
|
1601
1349
|
}
|
|
@@ -1625,24 +1373,29 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1625
1373
|
printSummary(tr, answers);
|
|
1626
1374
|
|
|
1627
1375
|
if (!yes) {
|
|
1628
|
-
const
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
initial: true,
|
|
1634
|
-
},
|
|
1635
|
-
{ onCancel: cancel }
|
|
1636
|
-
);
|
|
1376
|
+
const proceed = await ui.confirm({
|
|
1377
|
+
message: tr('new.firebase.confirm.proceed'),
|
|
1378
|
+
initialValue: true,
|
|
1379
|
+
onCancel: cancel,
|
|
1380
|
+
});
|
|
1637
1381
|
if (!proceed) {
|
|
1638
|
-
|
|
1382
|
+
ui.cancel(tr('prompt.cancelled'));
|
|
1639
1383
|
process.exit(0);
|
|
1640
1384
|
}
|
|
1641
1385
|
}
|
|
1642
1386
|
|
|
1643
1387
|
// ── Generate ────────────────────────────────────────────────────────────
|
|
1644
|
-
|
|
1645
|
-
|
|
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
|
+
};
|
|
1646
1399
|
|
|
1647
1400
|
let result;
|
|
1648
1401
|
try {
|
|
@@ -1656,9 +1409,7 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1656
1409
|
modules: answers.modules,
|
|
1657
1410
|
moduleAnswers,
|
|
1658
1411
|
language,
|
|
1659
|
-
onProgress
|
|
1660
|
-
spinner.text = kleur.cyan(stepProgress(key, language));
|
|
1661
|
-
},
|
|
1412
|
+
onProgress,
|
|
1662
1413
|
});
|
|
1663
1414
|
} else if (backend === 'api') {
|
|
1664
1415
|
result = await generateApiProject(targetDir, {
|
|
@@ -1669,9 +1420,7 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1669
1420
|
modules: answers.modules,
|
|
1670
1421
|
moduleAnswers,
|
|
1671
1422
|
language,
|
|
1672
|
-
onProgress
|
|
1673
|
-
spinner.text = kleur.cyan(stepProgress(key, language));
|
|
1674
|
-
},
|
|
1423
|
+
onProgress,
|
|
1675
1424
|
});
|
|
1676
1425
|
} else {
|
|
1677
1426
|
result = await generateFirebaseProject(targetDir, {
|
|
@@ -1684,14 +1433,14 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1684
1433
|
includeWeb: answers.includeWeb !== false,
|
|
1685
1434
|
functionsRegion: firebaseRegion,
|
|
1686
1435
|
language,
|
|
1687
|
-
onProgress
|
|
1688
|
-
spinner.text = kleur.cyan(stepProgress(key, language));
|
|
1689
|
-
},
|
|
1436
|
+
onProgress,
|
|
1690
1437
|
});
|
|
1691
1438
|
}
|
|
1692
|
-
|
|
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();
|
|
1693
1442
|
} catch (err) {
|
|
1694
|
-
|
|
1443
|
+
stepper.fail(err.message);
|
|
1695
1444
|
throw err;
|
|
1696
1445
|
}
|
|
1697
1446
|
|
|
@@ -1718,8 +1467,12 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1718
1467
|
}
|
|
1719
1468
|
|
|
1720
1469
|
// ── Results ─────────────────────────────────────────────────────────────
|
|
1721
|
-
|
|
1722
|
-
|
|
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));
|
|
1723
1476
|
|
|
1724
1477
|
// ── Supabase: link + db push + functions deploy (when project was created or existing selected) ─
|
|
1725
1478
|
const supabaseSetupPayload = (supabaseCreate && supabaseCreateResult?.ok)
|
|
@@ -1759,19 +1512,20 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1759
1512
|
// ── FCM Service Account key (best effort via gcloud — Firebase uses ADC, Supabase needs JSON) ──
|
|
1760
1513
|
let fcmServiceAccountJson = null;
|
|
1761
1514
|
if (answers.firebaseProjectId) {
|
|
1762
|
-
const fcmSpinner =
|
|
1515
|
+
const fcmSpinner = ui.spinner();
|
|
1516
|
+
fcmSpinner.start(tr('new.fcm.generating'));
|
|
1763
1517
|
const fcmResult = await createFcmServiceAccountKey(answers.firebaseProjectId);
|
|
1518
|
+
fcmSpinner.stop(tr('new.fcm.generating'));
|
|
1764
1519
|
if (fcmResult.ok) {
|
|
1765
1520
|
fcmServiceAccountJson = fcmResult.json;
|
|
1766
|
-
fcmSpinner.stop();
|
|
1767
1521
|
printStepResult({ name: 'fcm-key', ok: true }, language);
|
|
1768
1522
|
} else {
|
|
1769
|
-
fcmSpinner.stop();
|
|
1770
1523
|
printStepResult({ name: 'fcm-key', ok: false, detail: 'configure FIREBASE_SERVICE_ACCOUNT_JSON manualmente' }, language);
|
|
1771
1524
|
}
|
|
1772
1525
|
}
|
|
1773
1526
|
|
|
1774
|
-
const setupSpinner =
|
|
1527
|
+
const setupSpinner = ui.spinner();
|
|
1528
|
+
setupSpinner.start(tr('new.supabase.setup'));
|
|
1775
1529
|
try {
|
|
1776
1530
|
const secrets = {
|
|
1777
1531
|
firebaseProjectId: answers.firebaseProjectId,
|
|
@@ -1797,21 +1551,22 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1797
1551
|
secrets,
|
|
1798
1552
|
google
|
|
1799
1553
|
);
|
|
1800
|
-
setupSpinner.stop();
|
|
1554
|
+
setupSpinner.stop(tr('new.supabase.setup'));
|
|
1801
1555
|
setupSteps.forEach((s) => printStepResult(s, language));
|
|
1802
1556
|
} catch (err) {
|
|
1803
|
-
setupSpinner.
|
|
1804
|
-
|
|
1557
|
+
setupSpinner.error(err.message);
|
|
1558
|
+
ui.log.warn(tr('new.supabase.setupManual'));
|
|
1805
1559
|
}
|
|
1806
1560
|
}
|
|
1807
1561
|
}
|
|
1808
1562
|
|
|
1809
1563
|
// ── API: FCM Service Account key — save to .kasy/ for server configuration ──
|
|
1810
1564
|
if (backend === 'api' && answers.firebaseProjectId) {
|
|
1811
|
-
const fcmSpinner =
|
|
1565
|
+
const fcmSpinner = ui.spinner();
|
|
1566
|
+
fcmSpinner.start(tr('new.fcm.generating'));
|
|
1812
1567
|
const fcmResult = await createFcmServiceAccountKey(answers.firebaseProjectId);
|
|
1568
|
+
fcmSpinner.stop(tr('new.fcm.generating'));
|
|
1813
1569
|
if (fcmResult.ok) {
|
|
1814
|
-
fcmSpinner.stop();
|
|
1815
1570
|
try {
|
|
1816
1571
|
const kasyDir = path.join(targetDir, '.kasy');
|
|
1817
1572
|
await fs.ensureDir(kasyDir);
|
|
@@ -1826,12 +1581,11 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1826
1581
|
}
|
|
1827
1582
|
}
|
|
1828
1583
|
printStepResult({ name: 'fcm-key-saved', ok: true }, language);
|
|
1829
|
-
|
|
1584
|
+
ui.log.message(tr('new.fcm.serverConfig'));
|
|
1830
1585
|
} catch (_) {
|
|
1831
1586
|
printStepResult({ name: 'fcm-key-saved', ok: false, detail: 'salvar arquivo de chave falhou' }, language);
|
|
1832
1587
|
}
|
|
1833
1588
|
} else {
|
|
1834
|
-
fcmSpinner.stop();
|
|
1835
1589
|
printStepResult({ name: 'fcm-key', ok: false, detail: 'configure FIREBASE_SERVICE_ACCOUNT_JSON manualmente' }, language);
|
|
1836
1590
|
}
|
|
1837
1591
|
}
|
|
@@ -1844,9 +1598,10 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1844
1598
|
if (answers.firebaseProjectId && firebaseSetupMode === 'existing') {
|
|
1845
1599
|
const flutterfireOkForSha1 = result.steps.find((s) => s.name === 'flutterfire')?.ok;
|
|
1846
1600
|
if (flutterfireOkForSha1) {
|
|
1847
|
-
const sha1Spinner =
|
|
1601
|
+
const sha1Spinner = ui.spinner();
|
|
1602
|
+
sha1Spinner.start(tr('new.sha1.registering'));
|
|
1848
1603
|
const sha1Result = await registerDebugSha1(answers.firebaseProjectId, answers.bundleId);
|
|
1849
|
-
sha1Spinner.stop();
|
|
1604
|
+
sha1Spinner.stop(tr('new.sha1.registering'));
|
|
1850
1605
|
if (sha1Result.ok) {
|
|
1851
1606
|
if (!sha1Result.existed) {
|
|
1852
1607
|
printStepResult({ name: 'sha1', ok: true }, language);
|
|
@@ -1854,9 +1609,9 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1854
1609
|
// existed === true → already registered, silent success
|
|
1855
1610
|
} else {
|
|
1856
1611
|
const sha1ManualUrl = `https://console.firebase.google.com/project/${answers.firebaseProjectId}/settings/general/android:${answers.bundleId}`;
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
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
|
+
);
|
|
1860
1615
|
openUrl(sha1ManualUrl);
|
|
1861
1616
|
}
|
|
1862
1617
|
}
|
|
@@ -1865,14 +1620,9 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1865
1620
|
const flutterfireStep = result.steps.find((s) => s.name === 'flutterfire');
|
|
1866
1621
|
if (flutterfireStep && !flutterfireStep.ok) {
|
|
1867
1622
|
const platforms = answers.includeWeb !== false ? 'android,ios,web' : 'android,ios';
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
kleur.cyan(
|
|
1872
|
-
` dart pub global run flutterfire_cli:flutterfire configure` +
|
|
1873
|
-
` --project=${answers.firebaseProjectId}` +
|
|
1874
|
-
` --out lib/firebase_options_dev.dart --platforms=${platforms} --yes`
|
|
1875
|
-
)
|
|
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)}`
|
|
1876
1626
|
);
|
|
1877
1627
|
}
|
|
1878
1628
|
|
|
@@ -1880,16 +1630,15 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1880
1630
|
// Cannot be automated: the .p8 key only exists in Apple Developer Portal.
|
|
1881
1631
|
if (answers.firebaseProjectId) {
|
|
1882
1632
|
const apnsUrl = `https://console.firebase.google.com/project/${answers.firebaseProjectId}/settings/cloudmessaging`;
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
console.log(kleur.gray(` ${tr('new.apns.step2')}`));
|
|
1887
|
-
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
|
+
);
|
|
1888
1636
|
openUrl(apnsUrl);
|
|
1889
1637
|
}
|
|
1890
1638
|
|
|
1891
1639
|
printSuccessCard(tr, answers, targetDir);
|
|
1892
1640
|
|
|
1641
|
+
ui.outro(kleur.bold(tr('new.success.title')));
|
|
1893
1642
|
}
|
|
1894
1643
|
|
|
1895
1644
|
module.exports = { runNew };
|