kasy-cli 1.17.0 → 1.19.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 +16 -2
- package/lib/commands/add.js +7 -7
- package/lib/commands/configure.js +548 -0
- package/lib/commands/deploy.js +4 -4
- package/lib/commands/doctor.js +17 -0
- package/lib/commands/favicon.js +4 -4
- package/lib/commands/icon.js +5 -5
- package/lib/commands/new.js +483 -324
- package/lib/commands/run.js +17 -4
- package/lib/commands/splash.js +5 -5
- package/lib/commands/update.js +9 -9
- package/lib/scaffold/CHANGELOG.json +14 -0
- package/lib/scaffold/backends/firebase/enable-auth-via-cli.js +108 -0
- package/lib/scaffold/backends/firebase/setup-from-scratch.js +123 -5
- package/lib/scaffold/generate.js +24 -8
- package/lib/scaffold/shared/post-build.js +8 -0
- package/lib/utils/brand.js +16 -12
- package/lib/utils/flutter-run.js +139 -11
- package/lib/utils/i18n/messages-en.js +62 -5
- package/lib/utils/i18n/messages-es.js +62 -5
- package/lib/utils/i18n/messages-pt.js +63 -6
- package/lib/utils/ui.js +79 -4
- package/package.json +1 -2
- package/templates/firebase/README.en.md +1 -1
- package/templates/firebase/README.es.md +1 -1
- package/templates/firebase/README.md +1 -1
- package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/MainActivity.kt +0 -15
- package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/MyWidget.kt +65 -26
- package/templates/firebase/android/app/src/main/res/drawable/widget_add_button.xml +2 -2
- package/templates/firebase/android/app/src/main/res/drawable/widget_gradient_bg.xml +7 -2
- package/templates/firebase/android/app/src/main/res/drawable/widget_gradient_inner.xml +6 -6
- package/templates/firebase/android/app/src/main/res/drawable/widget_plan_pill_bg.xml +1 -1
- package/templates/firebase/android/app/src/main/res/drawable/widget_preview_image.xml +2 -2
- package/templates/firebase/android/app/src/main/res/drawable/widget_pro_pill_bg.xml +1 -1
- package/templates/firebase/android/app/src/main/res/drawable-hdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-hdpi/splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-mdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-mdpi/splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-hdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-hdpi/splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-mdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-mdpi/splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-xhdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-xhdpi/splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-xxhdpi/splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-xxxhdpi/splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-xhdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-xhdpi/splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-xxhdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-xxhdpi/splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/layout/widget_preview.xml +3 -3
- package/templates/firebase/android/app/src/main/res/values/colors.xml +32 -0
- package/templates/firebase/assets/images/splash_logo_dark.png +0 -0
- package/templates/firebase/assets/images/splash_logo_dark_android12.png +0 -0
- package/templates/firebase/assets/images/splash_logo_light.png +0 -0
- package/templates/firebase/assets/images/splash_logo_light_android12.png +0 -0
- package/templates/firebase/ios/HomeWidgetExtension/MyWidget.swift +75 -29
- package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark@2x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark@3x.png +0 -0
- package/templates/firebase/ios/Runner/Base.lproj/LaunchScreen.storyboard +1 -1
- package/templates/firebase/lib/components/components.dart +1 -0
- package/templates/firebase/lib/components/kasy_avatar.dart +65 -17
- package/templates/firebase/lib/components/kasy_avatar_presets.dart +121 -97
- package/templates/firebase/lib/components/kasy_button.dart +8 -8
- package/templates/firebase/lib/components/kasy_date_picker.dart +2173 -0
- package/templates/firebase/lib/components/kasy_tabs.dart +214 -91
- package/templates/firebase/lib/components/kasy_text_area.dart +9 -4
- package/templates/firebase/lib/components/kasy_text_field.dart +96 -36
- package/templates/firebase/lib/components/kasy_text_field_otp.dart +1 -2
- package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +88 -35
- package/templates/firebase/lib/core/bottom_menu/bottom_router.dart +7 -43
- package/templates/firebase/lib/core/dev_inspector/dev_inspector.dart +118 -16
- package/templates/firebase/lib/core/dev_inspector/dev_inspector_service.dart +14 -20
- package/templates/firebase/lib/core/home_widgets/home_widget_mywidget_service.dart +12 -18
- package/templates/firebase/lib/core/security/secured_storage.dart +56 -15
- package/templates/firebase/lib/core/theme/providers/theme_provider.dart +3 -0
- package/templates/firebase/lib/core/theme/web_background_sync.dart +3 -0
- package/templates/firebase/lib/core/theme/web_background_sync_web.dart +18 -0
- package/templates/firebase/lib/core/web_device_preview/web_device_preview.dart +3 -6
- package/templates/firebase/lib/features/authentication/api/authentication_api.dart +6 -0
- package/templates/firebase/lib/features/home/home_components_page.dart +3 -2
- package/templates/firebase/lib/features/home/home_components_preview_registry.dart +949 -77
- package/templates/firebase/lib/features/home/home_page.dart +17 -40
- package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +1 -16
- package/templates/firebase/lib/features/notifications/ui/widgets/empty_notifications.dart +0 -4
- package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +10 -5
- package/templates/firebase/lib/i18n/en.i18n.json +2 -1
- package/templates/firebase/lib/i18n/es.i18n.json +2 -1
- package/templates/firebase/lib/i18n/pt.i18n.json +2 -1
- package/templates/firebase/lib/main.dart +34 -34
- package/templates/firebase/pubspec.yaml +2 -1
- package/templates/firebase/storage.cors.json +8 -0
- package/templates/firebase/web/index.html +24 -2
- package/templates/firebase/web/splash/img/dark-1x.png +0 -0
- package/templates/firebase/web/splash/img/dark-2x.png +0 -0
- package/templates/firebase/web/splash/img/dark-3x.png +0 -0
- package/templates/firebase/web/splash/img/dark-4x.png +0 -0
- package/templates/firebase/web/splash/img/light-1x.png +0 -0
- package/templates/firebase/web/splash/img/light-2x.png +0 -0
- package/templates/firebase/web/splash/img/light-3x.png +0 -0
- package/templates/firebase/web/splash/img/light-4x.png +0 -0
- package/templates/firebase/lib/core/bottom_menu/kasy_bart_navigation.dart +0 -22
package/lib/commands/new.js
CHANGED
|
@@ -29,7 +29,7 @@ function openUrl(url) {
|
|
|
29
29
|
} catch (_) {}
|
|
30
30
|
}
|
|
31
31
|
const ui = require('../utils/ui');
|
|
32
|
-
const { printBanner,
|
|
32
|
+
const { printBanner, successBox, paintLime } = require('../utils/brand');
|
|
33
33
|
const fs = require('fs-extra');
|
|
34
34
|
const { createTranslator } = require('../utils/i18n');
|
|
35
35
|
const { getStoredLanguage, setStoredLanguage } = require('../utils/license');
|
|
@@ -42,7 +42,7 @@ const {
|
|
|
42
42
|
runChecks,
|
|
43
43
|
hasRequiredFailures,
|
|
44
44
|
} = require('../utils/checks');
|
|
45
|
-
const { normalizeBackend, getVisibleFeatures } = require('../scaffold/catalog');
|
|
45
|
+
const { normalizeBackend, getVisibleFeatures, FEATURE_CATALOG } = require('../scaffold/catalog');
|
|
46
46
|
|
|
47
47
|
// Audience gate: set KASY_INTERNAL=1 to reveal beta/internal features.
|
|
48
48
|
const KASY_AUDIENCE = process.env.KASY_INTERNAL === '1' ? 'internal' : 'public';
|
|
@@ -50,9 +50,10 @@ const { generateFirebaseProject } = require('../scaffold/backends/firebase/gener
|
|
|
50
50
|
const { generateSupabaseProject } = require('../scaffold/backends/supabase/generator');
|
|
51
51
|
const { generateApiProject } = require('../scaffold/backends/api/generator');
|
|
52
52
|
const { createProjectAndGetKeys, setupLinkedProject, checkLoggedIn, getOrgsList, getProjectsByOrg, getProjectKeys } = require('../scaffold/backends/supabase/deploy');
|
|
53
|
-
const { writeSupabaseGoogleAuthOptions, readSupabaseGoogleCredentials, getGoogleClientSecretViaGcloud } = require('../scaffold/shared/post-build');
|
|
53
|
+
const { writeSupabaseGoogleAuthOptions, readSupabaseGoogleCredentials, getGoogleClientSecretViaGcloud, flutterfireConfigure, writeGoogleAuthOptions, writeGoogleIosUrlScheme } = require('../scaffold/shared/post-build');
|
|
54
54
|
const { toPackageName } = require('../scaffold/backends/firebase/tokens');
|
|
55
55
|
const { setupFromScratch, setupExistingProject, listBillingAccounts, listGcpOrganizations, checkGcloudAuth, getGcloudInstallInstructions, enableAuthProviders, registerDebugSha1 } = require('../scaffold/backends/firebase/setup-from-scratch');
|
|
56
|
+
const { enableAuthViaFirebaseCli } = require('../scaffold/backends/firebase/enable-auth-via-cli');
|
|
56
57
|
const { createFcmServiceAccountKey } = require('../scaffold/shared/fcm-service-account');
|
|
57
58
|
|
|
58
59
|
// Região padrão para criação de projetos Supabase via API.
|
|
@@ -83,11 +84,9 @@ async function promptBillingAccountIfNeeded(tr, onCancel) {
|
|
|
83
84
|
};
|
|
84
85
|
|
|
85
86
|
if (!billingList.accounts?.length) {
|
|
86
|
-
ui.note(tr('new.firebase.q.billingAccount.context'));
|
|
87
87
|
return askManualId();
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
ui.note(tr('new.firebase.q.billingAccount.context'));
|
|
91
90
|
const billingAccountId = await ui.select({
|
|
92
91
|
message: tr('new.firebase.q.billingAccount'),
|
|
93
92
|
initialValue: billingList.accounts[0].id,
|
|
@@ -188,31 +187,12 @@ function printPrerequisites(tr, backend, firebaseSetupMode = 'existing', checkRe
|
|
|
188
187
|
}
|
|
189
188
|
}
|
|
190
189
|
|
|
191
|
-
function printSummary(tr, answers) {
|
|
192
|
-
const modules = answers.modules.length > 0
|
|
193
|
-
? answers.modules.join(', ')
|
|
194
|
-
: tr('new.firebase.confirm.none');
|
|
195
|
-
|
|
196
|
-
const backendLabel = { firebase: '🔥 Firebase', supabase: '🟢 Supabase', api: '🔗 API REST' }[answers.backend] || answers.backend;
|
|
197
|
-
|
|
198
|
-
const rows = [
|
|
199
|
-
`${kleur.dim(tr('new.firebase.confirm.app') + ':')} ${kleur.white(answers.appName)}`,
|
|
200
|
-
`${kleur.dim('Bundle:')} ${kleur.white(answers.bundleId)}`,
|
|
201
|
-
`${kleur.dim('Backend:')} ${kleur.white(backendLabel)}`,
|
|
202
|
-
];
|
|
203
|
-
if (answers.firebaseProjectId) rows.push(`${kleur.dim('Firebase:')} ${kleur.white(answers.firebaseProjectId)}`);
|
|
204
|
-
if (answers.supabaseUrl) rows.push(`${kleur.dim('Supabase URL:')} ${kleur.white(answers.supabaseUrl)}`);
|
|
205
|
-
if (answers.apiBaseUrl) rows.push(`${kleur.dim('API URL:')} ${kleur.white(answers.apiBaseUrl)}`);
|
|
206
|
-
rows.push(`${kleur.dim(tr('new.firebase.confirm.modules') + ':')} ${kleur.white(modules)}`);
|
|
207
|
-
|
|
208
|
-
console.log(infoBox(`📦 ${tr('new.firebase.confirm.title')}`, rows.join('\n')));
|
|
209
|
-
}
|
|
210
|
-
|
|
211
190
|
const STEP_LABELS = {
|
|
212
191
|
'project-setup': { en: 'Project configured', pt: 'Projeto configurado', es: 'Proyecto configurado' },
|
|
213
192
|
'pub-get': { en: 'Packages installed', pt: 'Pacotes instalados', es: 'Paquetes instalados' },
|
|
214
193
|
'slang': { en: 'Translations generated', pt: 'Traducoes geradas', es: 'Traducciones generadas' },
|
|
215
194
|
'build-runner': { en: 'Code generated (Riverpod / Freezed)', pt: 'Codigo gerado (Riverpod / Freezed)', es: 'Codigo generado (Riverpod / Freezed)' },
|
|
195
|
+
'dart-fix': { en: 'Lints auto-fixed', pt: 'Avisos de estilo corrigidos', es: 'Lints corregidos automaticamente' },
|
|
216
196
|
'flutterfire': { en: 'Firebase configured (flutterfire)', pt: 'Firebase configurado (flutterfire)', es: 'Firebase configurado (flutterfire)' },
|
|
217
197
|
'Service Account key': { en: 'Service Account key', pt: 'Chave de servico', es: 'Clave de servicio' },
|
|
218
198
|
'firebase_key.json': { en: 'Service account key copied', pt: 'Chave de conta de servico copiada', es: 'Clave de cuenta de servicio copiada' },
|
|
@@ -232,6 +212,7 @@ const STEP_LABELS = {
|
|
|
232
212
|
'supabase db push': { en: 'Database migrated', pt: 'Banco migrado', es: 'Base de datos migrada' },
|
|
233
213
|
'anonymous sign-in': { en: 'Anonymous sign-in enabled', pt: 'Login anonimo ativado', es: 'Inicio de sesion anonimo activado' },
|
|
234
214
|
'google sign-in': { en: 'Google Sign-In enabled', pt: 'Google Sign-In ativado', es: 'Google Sign-In activado' },
|
|
215
|
+
'apple sign-in': { en: 'Apple Sign-In ready (configure credentials before testing — kasy.dev/docs/apple)', pt: 'Apple Sign-In preparado (configure as credenciais antes de testar — kasy.dev/docs/apple)', es: 'Apple Sign-In listo (configura las credenciales antes de probar — kasy.dev/docs/apple)' },
|
|
235
216
|
'google-auth-options': { en: 'Google client IDs written', pt: 'Client IDs do Google gravados', es: 'Client IDs de Google escritos' },
|
|
236
217
|
'ios-url-scheme': { en: 'iOS URL scheme registered', pt: 'URL scheme iOS registrado', es: 'URL scheme iOS registrado' },
|
|
237
218
|
'google-ios-url-scheme': { en: 'iOS Google URL scheme registered', pt: 'URL scheme Google iOS registrado', es: 'URL scheme Google iOS registrado' },
|
|
@@ -253,6 +234,7 @@ const STEP_LABELS = {
|
|
|
253
234
|
'service-account': { en: 'Service account key created', pt: 'Chave de conta de servico criada', es: 'Clave de cuenta de servicio creada' },
|
|
254
235
|
'firestore': { en: 'Firestore database created', pt: 'Banco Firestore criado', es: 'Base de datos Firestore creada' },
|
|
255
236
|
'storage': { en: 'Firebase Storage bucket created', pt: 'Bucket Firebase Storage criado', es: 'Bucket Firebase Storage creado' },
|
|
237
|
+
'storage-cors': { en: 'CORS enabled on Storage (web images)', pt: 'CORS ativado no Storage (imagens na web)', es: 'CORS activado en Storage (imágenes en web)' },
|
|
256
238
|
};
|
|
257
239
|
|
|
258
240
|
const STEP_PROGRESS = {
|
|
@@ -260,6 +242,7 @@ const STEP_PROGRESS = {
|
|
|
260
242
|
'pub-get': { en: 'Installing packages… (may take a few minutes)', pt: 'Instalando pacotes… (pode levar alguns minutos)', es: 'Instalando paquetes… (puede tardar varios minutos)' },
|
|
261
243
|
'slang': { en: 'Generating translations…', pt: 'Gerando traducoes…', es: 'Generando traducciones…' },
|
|
262
244
|
'build-runner': { en: 'Generating code (Riverpod / Freezed)… (may take a few minutes)', pt: 'Gerando codigo (Riverpod / Freezed)… (pode demorar)', es: 'Generando codigo (Riverpod / Freezed)… (puede tardar)' },
|
|
245
|
+
'dart-fix': { en: 'Auto-fixing lints…', pt: 'Corrigindo avisos de estilo…', es: 'Corrigiendo lints automaticamente…' },
|
|
263
246
|
'flutterfire': { en: 'Connecting to Firebase…', pt: 'Conectando ao Firebase…', es: 'Conectando a Firebase…' },
|
|
264
247
|
'deploy': { en: 'Deploying backend to Firebase…', pt: 'Publicando backend no Firebase…', es: 'Desplegando backend en Firebase…' },
|
|
265
248
|
'gcp-project': { en: 'Creating GCP project…', pt: 'Criando projeto GCP…', es: 'Creando proyecto GCP…' },
|
|
@@ -273,6 +256,7 @@ const STEP_PROGRESS = {
|
|
|
273
256
|
'service-account': { en: 'Creating service account key…', pt: 'Criando chave de conta de servico…', es: 'Creando clave de cuenta de servicio…' },
|
|
274
257
|
'firestore': { en: 'Creating Firestore database…', pt: 'Criando banco Firestore…', es: 'Creando base de datos Firestore…' },
|
|
275
258
|
'storage': { en: 'Creating Firebase Storage bucket…', pt: 'Criando bucket Firebase Storage…', es: 'Creando bucket Firebase Storage…' },
|
|
259
|
+
'storage-cors': { en: 'Enabling CORS on Storage…', pt: 'Ativando CORS no Storage…', es: 'Activando CORS en Storage…' },
|
|
276
260
|
'deploy-retry-wait': { en: 'Waiting 4 min for GCP permissions to propagate… (do not close terminal)', pt: 'Aguardando 4 min para permissões do GCP propagarem… (não feche o terminal)', es: 'Esperando 4 min para propagar permisos de GCP… (no cierres la terminal)' },
|
|
277
261
|
'deploy-retry-wait-2': { en: 'Retrying with updated permissions, 2 more min… (almost there!)', pt: 'Tentando novamente com permissões atualizadas, mais 2 min… (quase lá!)', es: 'Reintentando con permisos actualizados, 2 min más… (¡casi listo!)' },
|
|
278
262
|
};
|
|
@@ -324,32 +308,46 @@ function printCreateFromScratchStatus(result, tr) {
|
|
|
324
308
|
|
|
325
309
|
function printSuccessCard(tr, answers, targetDir) {
|
|
326
310
|
const folderName = path.basename(targetDir);
|
|
327
|
-
const consoleUrl = answers.backend === 'firebase' && answers.firebaseProjectId
|
|
328
|
-
? `https://console.firebase.google.com/project/${answers.firebaseProjectId}`
|
|
329
|
-
: answers.backend === 'supabase'
|
|
330
|
-
? 'https://supabase.com/dashboard'
|
|
331
|
-
: answers.apiBaseUrl || null;
|
|
332
311
|
|
|
333
312
|
const lines = [];
|
|
313
|
+
|
|
314
|
+
if (answers.modules?.length > 0) {
|
|
315
|
+
const byId = Object.fromEntries(FEATURE_CATALOG.map((f) => [f.id, f]));
|
|
316
|
+
const visible = answers.modules.filter((id) => byId[id]);
|
|
317
|
+
if (visible.length > 0) {
|
|
318
|
+
lines.push(paintLime(`✓ ${tr('new.success.featuresInstalled')}`));
|
|
319
|
+
for (const id of visible) {
|
|
320
|
+
const name = byId[id]?.displayName || id;
|
|
321
|
+
lines.push(` ${kleur.dim('•')} ${name}`);
|
|
322
|
+
}
|
|
323
|
+
lines.push('');
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
334
327
|
lines.push(kleur.bold(tr('new.success.nextSteps')));
|
|
335
328
|
lines.push('');
|
|
336
|
-
|
|
329
|
+
|
|
330
|
+
let stepNum = 1;
|
|
331
|
+
lines.push(`${kleur.dim(`${stepNum++}.`)} ${kleur.dim(tr('new.success.step.cd'))}`);
|
|
337
332
|
lines.push(` ${kleur.cyan(`cd ${folderName}`)}`);
|
|
338
|
-
|
|
333
|
+
lines.push('');
|
|
334
|
+
|
|
339
335
|
if (answers.backend === 'firebase') {
|
|
340
|
-
lines.push('');
|
|
341
336
|
lines.push(`${kleur.dim(`${stepNum++}.`)} ${kleur.dim(tr('new.success.step.deploy'))}`);
|
|
342
337
|
lines.push(` ${kleur.cyan('kasy deploy')}`);
|
|
338
|
+
lines.push('');
|
|
343
339
|
}
|
|
340
|
+
|
|
341
|
+
lines.push(`${kleur.dim(`${stepNum++}.`)} ${kleur.dim(tr('new.success.step.configure'))}`);
|
|
342
|
+
lines.push(` ${kleur.cyan('kasy configure')}`);
|
|
344
343
|
lines.push('');
|
|
344
|
+
|
|
345
345
|
lines.push(`${kleur.dim(`${stepNum++}.`)} ${kleur.dim(tr('new.success.step.run'))}`);
|
|
346
|
-
lines.push(` ${kleur.cyan('kasy run')}`);
|
|
347
|
-
lines.push(
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
lines.push(` ${kleur.cyan(consoleUrl)}`);
|
|
352
|
-
}
|
|
346
|
+
lines.push(` ${kleur.cyan('kasy run')} ${kleur.dim(tr('new.success.step.run.vscode'))}`);
|
|
347
|
+
lines.push('');
|
|
348
|
+
|
|
349
|
+
lines.push(`${kleur.dim(`${stepNum++}.`)} ${kleur.dim(tr('new.success.step.docs'))}`);
|
|
350
|
+
lines.push(` ${kleur.cyan('https://kasy.dev/docs')}`);
|
|
353
351
|
|
|
354
352
|
console.log(successBox(`🎉 ${tr('new.success.title')}`, lines.join('\n')));
|
|
355
353
|
}
|
|
@@ -464,9 +462,9 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
464
462
|
message: tr('new.q.backend'),
|
|
465
463
|
initialValue: 'firebase',
|
|
466
464
|
options: [
|
|
467
|
-
{ value: 'firebase', label: '🔥 Firebase'
|
|
468
|
-
{ value: 'supabase', label: '🟢 Supabase'
|
|
469
|
-
{ value: 'api', label: '🔗 API REST'
|
|
465
|
+
{ value: 'firebase', label: '🔥 Firebase' },
|
|
466
|
+
{ value: 'supabase', label: '🟢 Supabase' },
|
|
467
|
+
{ value: 'api', label: '🔗 API REST' },
|
|
470
468
|
],
|
|
471
469
|
onCancel: cancel,
|
|
472
470
|
});
|
|
@@ -504,59 +502,38 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
504
502
|
}
|
|
505
503
|
}
|
|
506
504
|
|
|
507
|
-
//
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
if (!
|
|
511
|
-
|
|
512
|
-
ui.cancel(tr('new.firebase.error.aborted'));
|
|
513
|
-
process.exit(1);
|
|
514
|
-
}
|
|
515
|
-
const appName = path.basename(targetDir);
|
|
516
|
-
const slug = appName
|
|
505
|
+
// Helper: slug → bundle id (e.g. "MeuApp" → "com.meuapp.app")
|
|
506
|
+
const deriveBundleId = (name) => {
|
|
507
|
+
const trimmed = (name || '').trim();
|
|
508
|
+
if (!trimmed) return 'com.example.app';
|
|
509
|
+
const slug = trimmed
|
|
517
510
|
.normalize('NFD')
|
|
518
511
|
.replace(/[̀-ͯ]/g, '')
|
|
519
512
|
.toLowerCase()
|
|
520
513
|
.replace(/[^a-z0-9]/g, '');
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
514
|
+
if (!slug || /^\d/.test(slug)) return 'com.example.app';
|
|
515
|
+
return `com.${slug}.app`;
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
// ── App name — derived from argv when given, otherwise asked once ──────────
|
|
519
|
+
let core;
|
|
520
|
+
if (hasExplicitDir) {
|
|
521
|
+
// `kasy new MeuApp` — use the argument as the name; don't ask again.
|
|
522
|
+
const appName = path.basename(targetDir);
|
|
523
|
+
core = { appName, bundleId: deriveBundleId(appName) };
|
|
524
|
+
ui.log.info(`App: ${kleur.white(core.appName)}`);
|
|
525
|
+
} else if (yes) {
|
|
526
|
+
ui.log.error('--yes requires an app name: kasy new MyApp --yes');
|
|
527
|
+
ui.cancel(tr('new.firebase.error.aborted'));
|
|
528
|
+
process.exit(1);
|
|
525
529
|
} else {
|
|
526
530
|
const appName = await ui.text({
|
|
527
531
|
message: tr('new.firebase.q.appName'),
|
|
528
532
|
placeholder: tr('new.firebase.q.appName.hint'),
|
|
529
|
-
initialValue: hasExplicitDir ? path.basename(targetDir) : '',
|
|
530
533
|
validate: (v) => (v && v.trim() ? undefined : tr('new.firebase.q.appName.required')),
|
|
531
534
|
onCancel: cancel,
|
|
532
535
|
});
|
|
533
|
-
|
|
534
|
-
const defaultBundleId = (() => {
|
|
535
|
-
const name = (appName || '').trim();
|
|
536
|
-
if (!name) return 'com.example.app';
|
|
537
|
-
const slug = name
|
|
538
|
-
.normalize('NFD')
|
|
539
|
-
.replace(/[̀-ͯ]/g, '')
|
|
540
|
-
.toLowerCase()
|
|
541
|
-
.replace(/[^a-z0-9]/g, '');
|
|
542
|
-
if (!slug || /^\d/.test(slug)) return 'com.example.app';
|
|
543
|
-
return `com.${slug}.app`;
|
|
544
|
-
})();
|
|
545
|
-
|
|
546
|
-
const bundleId = await ui.text({
|
|
547
|
-
message: tr('new.firebase.q.bundleId'),
|
|
548
|
-
placeholder: tr('new.firebase.q.bundleId.hint'),
|
|
549
|
-
initialValue: defaultBundleId,
|
|
550
|
-
validate: (v) => {
|
|
551
|
-
if (!v || !v.trim()) return tr('new.firebase.q.bundleId.required');
|
|
552
|
-
return /^[a-zA-Z][\w]*(\.[a-zA-Z][\w]*)+$/.test(v.trim())
|
|
553
|
-
? undefined
|
|
554
|
-
: tr('new.firebase.q.bundleId.invalid');
|
|
555
|
-
},
|
|
556
|
-
onCancel: cancel,
|
|
557
|
-
});
|
|
558
|
-
|
|
559
|
-
core = { appName, bundleId };
|
|
536
|
+
core = { appName, bundleId: deriveBundleId(appName) };
|
|
560
537
|
}
|
|
561
538
|
|
|
562
539
|
// Resolve targetDir now that we have the app name
|
|
@@ -571,10 +548,99 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
571
548
|
}
|
|
572
549
|
}
|
|
573
550
|
|
|
574
|
-
// ──
|
|
575
|
-
|
|
576
|
-
let firebaseSetupMode = 'existing';
|
|
551
|
+
// ── Wizard mode — Quick (zero config, recommended) or Step-by-step (all options) ─
|
|
552
|
+
let isQuick = yes; // --yes implies Quick mode
|
|
577
553
|
if (!yes) {
|
|
554
|
+
const wizardMode = await ui.select({
|
|
555
|
+
message: tr('new.q.mode'),
|
|
556
|
+
initialValue: 'quick',
|
|
557
|
+
options: [
|
|
558
|
+
{ value: 'quick', label: tr('new.q.mode.quick') },
|
|
559
|
+
{ value: 'advanced', label: tr('new.q.mode.advanced') },
|
|
560
|
+
],
|
|
561
|
+
onCancel: cancel,
|
|
562
|
+
});
|
|
563
|
+
isQuick = wizardMode === 'quick';
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// ── gcloud is required for Quick mode (creates GCP project + enables Auth via API) ─
|
|
567
|
+
// Failing here, before the project is generated, is much friendlier than dying mid-flow.
|
|
568
|
+
if (isQuick) {
|
|
569
|
+
const gcloudCheck = await checkGcloudAuth();
|
|
570
|
+
if (!gcloudCheck.ok) {
|
|
571
|
+
ui.log.error(tr('new.firebase.create.gcloudRequired'));
|
|
572
|
+
if (gcloudCheck.missing === 'gcloud') {
|
|
573
|
+
const instructions = getGcloudInstallInstructions();
|
|
574
|
+
const noteLines = [tr('new.firebase.create.installTitle')];
|
|
575
|
+
if (instructions.install) noteLines.push(`${tr('new.firebase.create.installCommand')}:\n ${kleur.cyan(instructions.install)}`);
|
|
576
|
+
if (instructions.hint) noteLines.push(instructions.hint);
|
|
577
|
+
noteLines.push(`${tr('new.firebase.create.installAfter')}:\n ${kleur.cyan(instructions.after)}`);
|
|
578
|
+
noteLines.push(`${tr('new.firebase.create.installUrl')}: ${instructions.url}`);
|
|
579
|
+
ui.note(noteLines.join('\n\n'));
|
|
580
|
+
} else {
|
|
581
|
+
ui.note(tr('new.firebase.create.authCommand'));
|
|
582
|
+
}
|
|
583
|
+
ui.cancel(tr('new.firebase.error.aborted'));
|
|
584
|
+
process.exit(1);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// ── Billing account check — Firebase needs an active billing account (Blaze) ─
|
|
588
|
+
// Without it, project creation succeeds but Storage and Cloud Functions fail later.
|
|
589
|
+
// Catching it here saves the user from a confusing mid-flow error.
|
|
590
|
+
const ensureBilling = async () => {
|
|
591
|
+
const billing = await listBillingAccounts();
|
|
592
|
+
if (billing.ok && billing.accounts?.length > 0) return true;
|
|
593
|
+
const billingUrl = 'https://console.cloud.google.com/billing/create';
|
|
594
|
+
ui.log.warn(tr('new.firebase.billing.required'));
|
|
595
|
+
ui.note(`${tr('new.firebase.billing.create.steps')}\n${kleur.cyan(billingUrl)}`);
|
|
596
|
+
openUrl(billingUrl);
|
|
597
|
+
const ready = await ui.confirm({
|
|
598
|
+
message: tr('new.firebase.billing.created.ready'),
|
|
599
|
+
initialValue: true,
|
|
600
|
+
onCancel: cancel,
|
|
601
|
+
});
|
|
602
|
+
if (!ready) {
|
|
603
|
+
ui.cancel(tr('new.firebase.error.aborted'));
|
|
604
|
+
process.exit(0);
|
|
605
|
+
}
|
|
606
|
+
const recheck = await listBillingAccounts();
|
|
607
|
+
if (!recheck.ok || !recheck.accounts?.length) {
|
|
608
|
+
ui.log.error(tr('new.firebase.billing.stillMissing'));
|
|
609
|
+
ui.cancel(tr('new.firebase.error.aborted'));
|
|
610
|
+
process.exit(1);
|
|
611
|
+
}
|
|
612
|
+
return true;
|
|
613
|
+
};
|
|
614
|
+
await ensureBilling();
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Visible section header for Advanced — helps the user track where they are.
|
|
618
|
+
const section = (key) => {
|
|
619
|
+
if (!isQuick) ui.log.info(paintLime(`── ${tr(key)} ──`));
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
// ── Bundle ID — Quick uses derived value silently; Step-by-step lets user override ─
|
|
623
|
+
section('new.advanced.section.config');
|
|
624
|
+
if (!isQuick) {
|
|
625
|
+
const bundleId = await ui.text({
|
|
626
|
+
message: tr('new.firebase.q.bundleId'),
|
|
627
|
+
placeholder: tr('new.firebase.q.bundleId.hint'),
|
|
628
|
+
initialValue: core.bundleId,
|
|
629
|
+
validate: (v) => {
|
|
630
|
+
if (!v || !v.trim()) return tr('new.firebase.q.bundleId.required');
|
|
631
|
+
return /^[a-zA-Z][\w]*(\.[a-zA-Z][\w]*)+$/.test(v.trim())
|
|
632
|
+
? undefined
|
|
633
|
+
: tr('new.firebase.q.bundleId.invalid');
|
|
634
|
+
},
|
|
635
|
+
onCancel: cancel,
|
|
636
|
+
});
|
|
637
|
+
core.bundleId = bundleId;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// ── Firebase setup mode — Quick always creates a new project ───────────────
|
|
641
|
+
// (--yes implies Quick, so it also defaults to creating a fresh project.)
|
|
642
|
+
let firebaseSetupMode = 'create';
|
|
643
|
+
if (!isQuick) {
|
|
578
644
|
if (backend === 'firebase') {
|
|
579
645
|
firebaseSetupMode = await ui.select({
|
|
580
646
|
message: tr('new.firebase.q.setupMode'),
|
|
@@ -599,25 +665,13 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
599
665
|
}
|
|
600
666
|
}
|
|
601
667
|
|
|
602
|
-
// ──
|
|
603
|
-
//
|
|
604
|
-
|
|
605
|
-
if (!
|
|
606
|
-
|
|
607
|
-
message: tr('new.q.mode'),
|
|
608
|
-
initialValue: 'quick',
|
|
609
|
-
options: [
|
|
610
|
-
{ value: 'quick', label: tr('new.q.mode.quick') },
|
|
611
|
-
{ value: 'advanced', label: tr('new.q.mode.advanced') },
|
|
612
|
-
],
|
|
613
|
-
onCancel: cancel,
|
|
614
|
-
});
|
|
615
|
-
isQuick = wizardMode === 'quick';
|
|
668
|
+
// ── Backend-specific prerequisites — only shown in Step-by-step mode ──────
|
|
669
|
+
// Quick mode hides this list: it intimidates beginners and the CLI handles
|
|
670
|
+
// most prerequisites automatically (gcloud check below, billing prompt, etc.).
|
|
671
|
+
if (!isQuick) {
|
|
672
|
+
printPrerequisites(tr, backend, firebaseSetupMode, backendCheckResults);
|
|
616
673
|
}
|
|
617
674
|
|
|
618
|
-
// ── Backend-specific prerequisites (dynamic: only show what's not yet verified) ─
|
|
619
|
-
printPrerequisites(tr, backend, firebaseSetupMode, backendCheckResults);
|
|
620
|
-
|
|
621
675
|
// ── Firebase project ID (if using an existing project) ──────────────────────
|
|
622
676
|
if (!yes) {
|
|
623
677
|
const needFirebaseProjectId =
|
|
@@ -639,19 +693,12 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
639
693
|
core.firebaseProjectId = firebaseProjectId;
|
|
640
694
|
}
|
|
641
695
|
} else {
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
onCancel: cancel,
|
|
649
|
-
});
|
|
650
|
-
firebaseProjectId = pid?.trim() || '';
|
|
651
|
-
}
|
|
652
|
-
if (firebaseProjectId) {
|
|
653
|
-
core.firebaseProjectId = firebaseProjectId;
|
|
654
|
-
ui.log.info(`Project: ${kleur.white(firebaseProjectId)}`);
|
|
696
|
+
// --yes mode: if --project was passed, reuse that existing project.
|
|
697
|
+
// Otherwise stay in 'create' mode and let setupFromScratch handle creation silently.
|
|
698
|
+
if (projectHint?.trim()) {
|
|
699
|
+
firebaseSetupMode = 'existing';
|
|
700
|
+
core.firebaseProjectId = projectHint.trim();
|
|
701
|
+
ui.log.info(`Project: ${kleur.white(core.firebaseProjectId)}`);
|
|
655
702
|
}
|
|
656
703
|
}
|
|
657
704
|
|
|
@@ -674,11 +721,13 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
674
721
|
// ── Firebase: create from scratch (when selected) ─────────────────────────
|
|
675
722
|
let firebaseIncludeWeb = true;
|
|
676
723
|
if (backend === 'firebase' && firebaseSetupMode === 'create') {
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
724
|
+
if (!isQuick) {
|
|
725
|
+
firebaseIncludeWeb = await ui.confirm({
|
|
726
|
+
message: tr('new.firebase.create.includeWeb'),
|
|
727
|
+
initialValue: true,
|
|
728
|
+
onCancel: cancel,
|
|
729
|
+
});
|
|
730
|
+
}
|
|
682
731
|
const gcloudCheck = await checkGcloudAuth();
|
|
683
732
|
if (!gcloudCheck.ok) {
|
|
684
733
|
ui.log.error(tr('new.firebase.create.gcloudRequired'));
|
|
@@ -701,30 +750,55 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
701
750
|
onCancel: cancel,
|
|
702
751
|
});
|
|
703
752
|
} else {
|
|
753
|
+
// Warn about duration + network before the org/billing prompts so the user
|
|
754
|
+
// can step away (or check the wifi) before the long-running call starts.
|
|
755
|
+
ui.log.info(tr('new.firebase.create.estimatedTime'));
|
|
704
756
|
const selectedOrgId = await promptOrganizationIfNeeded(tr, cancel);
|
|
705
757
|
let selectedBillingId = await promptBillingAccountIfNeeded(tr, cancel);
|
|
706
|
-
ui.
|
|
707
|
-
const ps1 = ui.makeTimedStepper();
|
|
758
|
+
const ps1 = ui.makeQuickStepper({ color: paintLime });
|
|
708
759
|
ps1.next(tr('new.firebase.create.creating'));
|
|
709
|
-
const
|
|
760
|
+
const ps1OnProgress = (key, data) => {
|
|
761
|
+
if (key === 'wait-propagate') {
|
|
762
|
+
ps1.next(tr('new.firebase.create.waitPropagate'));
|
|
763
|
+
} else if (key === 'firestore') {
|
|
764
|
+
ps1.next(stepProgress('firestore', language));
|
|
765
|
+
} else if (key === 'storage') {
|
|
766
|
+
ps1.next(stepProgress('storage', language));
|
|
767
|
+
} else if (key === 'storage-cors') {
|
|
768
|
+
ps1.next(stepProgress('storage-cors', language));
|
|
769
|
+
} else if (key === 'storage-cors-warn') {
|
|
770
|
+
ps1.stop();
|
|
771
|
+
ui.log.warn(`Storage CORS: ${(data?.error || '').slice(0, 80)}\n${kleur.cyan(data?.url || '')}`);
|
|
772
|
+
} else if (key === 'auth-providers-warn') {
|
|
773
|
+
ps1.stop();
|
|
774
|
+
ui.log.warn(`${tr('new.firebase.interactive.authWarn')}\n${kleur.cyan(data?.url || '')}`);
|
|
775
|
+
}
|
|
776
|
+
};
|
|
777
|
+
let setupResult = await setupFromScratch(core.appName.trim(), core.bundleId.trim(), {
|
|
710
778
|
includeWeb: firebaseIncludeWeb,
|
|
711
779
|
region: firebaseRegion,
|
|
712
780
|
tr,
|
|
713
781
|
billingAccountId: selectedBillingId || undefined,
|
|
714
782
|
organizationId: selectedOrgId || undefined,
|
|
715
|
-
onProgress:
|
|
716
|
-
if (key === 'wait-propagate') {
|
|
717
|
-
ps1.next(tr('new.firebase.create.waitPropagate'));
|
|
718
|
-
} else if (key === 'firestore') {
|
|
719
|
-
ps1.next(stepProgress('firestore', language));
|
|
720
|
-
} else if (key === 'storage') {
|
|
721
|
-
ps1.next(stepProgress('storage', language));
|
|
722
|
-
} else if (key === 'auth-providers-warn') {
|
|
723
|
-
ps1.stop();
|
|
724
|
-
ui.log.warn(`${tr('new.firebase.interactive.authWarn')}\n${kleur.cyan(data?.url || '')}`);
|
|
725
|
-
}
|
|
726
|
-
},
|
|
783
|
+
onProgress: ps1OnProgress,
|
|
727
784
|
});
|
|
785
|
+
|
|
786
|
+
// Silent retry for transient billing-quota errors (propagation delay,
|
|
787
|
+
// race between createProject and linkBilling). Keeps the same spinner
|
|
788
|
+
// line so the user doesn't see the red error if it succeeds on retry.
|
|
789
|
+
if (!setupResult.ok && setupResult.billingFailed && setupResult.billingQuotaError && setupResult.projectId) {
|
|
790
|
+
ps1.update(tr('new.firebase.create.billingWait'));
|
|
791
|
+
await new Promise((r) => setTimeout(r, 15000));
|
|
792
|
+
setupResult = await setupFromScratch(core.appName.trim(), core.bundleId.trim(), {
|
|
793
|
+
includeWeb: firebaseIncludeWeb,
|
|
794
|
+
region: firebaseRegion,
|
|
795
|
+
tr,
|
|
796
|
+
resumeFromBilling: { projectId: setupResult.projectId },
|
|
797
|
+
billingAccountId: selectedBillingId || undefined,
|
|
798
|
+
organizationId: selectedOrgId || undefined,
|
|
799
|
+
onProgress: ps1OnProgress,
|
|
800
|
+
});
|
|
801
|
+
}
|
|
728
802
|
const askReady = async (readyKey) => {
|
|
729
803
|
const ok = await ui.confirm({
|
|
730
804
|
message: tr(readyKey),
|
|
@@ -748,26 +822,25 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
748
822
|
ui.log.info(`Project ID: ${core.firebaseProjectId}`);
|
|
749
823
|
printCreateFromScratchStatus(setupResult, tr);
|
|
750
824
|
const authUrl = `https://console.firebase.google.com/project/${core.firebaseProjectId}/authentication/providers`;
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
? 'new.firebase.create.beforeContinue.step1'
|
|
757
|
-
: 'new.firebase.create.beforeContinue.step1.noAuth';
|
|
758
|
-
const readyKey = setupResult.googleSignInSkipped
|
|
759
|
-
? 'new.firebase.create.beforeContinue.ready'
|
|
760
|
-
: 'new.firebase.create.beforeContinue.ready.noAuth';
|
|
761
|
-
showBeforeContinue(step1Key, authUrl);
|
|
825
|
+
// Stay silent when Google is the only thing missing — the post-flutterfire
|
|
826
|
+
// retry will activate it once the OAuth Web Client exists. We only warn
|
|
827
|
+
// here when Email/Anonymous themselves failed (which is the rare path).
|
|
828
|
+
if (!setupResult.authEnabled) {
|
|
829
|
+
showBeforeContinue('new.firebase.create.beforeContinue.step1.noAuth', authUrl);
|
|
762
830
|
openUrl(authUrl);
|
|
763
|
-
|
|
831
|
+
if (!isQuick) {
|
|
832
|
+
await askReady('new.firebase.create.beforeContinue.ready.noAuth');
|
|
833
|
+
}
|
|
764
834
|
}
|
|
765
835
|
|
|
766
836
|
} else {
|
|
767
837
|
ps1.fail(tr('new.firebase.create.failed'));
|
|
768
838
|
let lastResult = setupResult;
|
|
769
839
|
while (lastResult.billingFailed && lastResult.projectId) {
|
|
770
|
-
|
|
840
|
+
const errLine = lastResult.billingQuotaError
|
|
841
|
+
? tr('new.firebase.create.billingQuotaError')
|
|
842
|
+
: `${tr('new.firebase.create.failed')}: ${lastResult.error}`;
|
|
843
|
+
ui.log.error(errLine);
|
|
771
844
|
ui.note(
|
|
772
845
|
`${kleur.cyan(lastResult.billingManualLink)}\n\n${tr('new.firebase.create.billingRetry.hint')}`,
|
|
773
846
|
tr('new.firebase.create.billingRetry.title')
|
|
@@ -784,7 +857,7 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
784
857
|
}
|
|
785
858
|
ui.log.message(tr('new.firebase.create.billingRetry.retrying'));
|
|
786
859
|
selectedBillingId = await promptBillingAccountIfNeeded(tr, cancel);
|
|
787
|
-
const ps2 = ui.
|
|
860
|
+
const ps2 = ui.makeQuickStepper({ color: paintLime });
|
|
788
861
|
ps2.next(tr('new.firebase.create.creating'));
|
|
789
862
|
lastResult = await setupFromScratch(core.appName.trim(), core.bundleId.trim(), {
|
|
790
863
|
includeWeb: firebaseIncludeWeb,
|
|
@@ -809,19 +882,14 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
809
882
|
ui.log.info(`Project ID: ${core.firebaseProjectId}`);
|
|
810
883
|
printCreateFromScratchStatus(lastResult, tr);
|
|
811
884
|
const authUrl = `https://console.firebase.google.com/project/${core.firebaseProjectId}/authentication/providers`;
|
|
812
|
-
if (lastResult.authEnabled
|
|
813
|
-
|
|
814
|
-
} else {
|
|
815
|
-
const step1Key = lastResult.googleSignInSkipped
|
|
816
|
-
? 'new.firebase.create.beforeContinue.step1'
|
|
817
|
-
: 'new.firebase.create.beforeContinue.step1.noAuth';
|
|
818
|
-
const lastReadyKey = lastResult.googleSignInSkipped
|
|
819
|
-
? 'new.firebase.create.beforeContinue.ready'
|
|
820
|
-
: 'new.firebase.create.beforeContinue.ready.noAuth';
|
|
821
|
-
showBeforeContinue(step1Key, authUrl);
|
|
885
|
+
if (!lastResult.authEnabled) {
|
|
886
|
+
showBeforeContinue('new.firebase.create.beforeContinue.step1.noAuth', authUrl);
|
|
822
887
|
openUrl(authUrl);
|
|
823
|
-
|
|
888
|
+
if (!isQuick) {
|
|
889
|
+
await askReady('new.firebase.create.beforeContinue.ready.noAuth');
|
|
890
|
+
}
|
|
824
891
|
}
|
|
892
|
+
// Google Sign-In status is reported after flutterfire (retry path).
|
|
825
893
|
|
|
826
894
|
break;
|
|
827
895
|
} else {
|
|
@@ -874,10 +942,11 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
874
942
|
onCancel: cancel,
|
|
875
943
|
});
|
|
876
944
|
} else {
|
|
945
|
+
// Warn before the org/billing prompts so the user is ready for the long call.
|
|
946
|
+
ui.log.info(tr('new.firebase.create.estimatedTime'));
|
|
877
947
|
const selectedOrgId = await promptOrganizationIfNeeded(tr, cancel);
|
|
878
948
|
const selectedBillingId = await promptBillingAccountIfNeeded(tr, cancel);
|
|
879
|
-
ui.
|
|
880
|
-
const ps3 = ui.makeTimedStepper();
|
|
949
|
+
const ps3 = ui.makeQuickStepper({ color: paintLime });
|
|
881
950
|
ps3.next(tr('new.firebase.create.creatingPush'));
|
|
882
951
|
const setupResult = await setupFromScratch(core.appName.trim(), core.bundleId.trim(), {
|
|
883
952
|
includeWeb: true,
|
|
@@ -926,15 +995,19 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
926
995
|
let supabaseExistingResult = null;
|
|
927
996
|
|
|
928
997
|
if (backend === 'supabase') {
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
998
|
+
if (isQuick) {
|
|
999
|
+
supabaseCreate = true;
|
|
1000
|
+
} else {
|
|
1001
|
+
supabaseCreate = await ui.select({
|
|
1002
|
+
message: tr('new.supabase.q.create'),
|
|
1003
|
+
initialValue: true,
|
|
1004
|
+
options: [
|
|
1005
|
+
{ value: true, label: tr('new.supabase.q.create.create') },
|
|
1006
|
+
{ value: false, label: tr('new.supabase.q.create.existing') },
|
|
1007
|
+
],
|
|
1008
|
+
onCancel: cancel,
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
938
1011
|
|
|
939
1012
|
const showLoginRequired = () => {
|
|
940
1013
|
ui.log.warn(`${tr('new.supabase.loginRequired')}\n${kleur.cyan(tr('new.supabase.loginCommand'))}`);
|
|
@@ -971,12 +1044,19 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
971
1044
|
});
|
|
972
1045
|
supabaseRegion = region || DEFAULT_SUPABASE_REGION;
|
|
973
1046
|
}
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
1047
|
+
if (isQuick) {
|
|
1048
|
+
// Quick mode: generate a strong password and save to .kasy/supabase.json after build.
|
|
1049
|
+
// The user can read it from there or rotate later via Supabase dashboard.
|
|
1050
|
+
supabaseDbPassword = crypto.randomBytes(18).toString('base64')
|
|
1051
|
+
.replace(/[+/=]/g, '')
|
|
1052
|
+
.slice(0, 24);
|
|
1053
|
+
} else {
|
|
1054
|
+
supabaseDbPassword = await ui.password({
|
|
1055
|
+
message: tr('new.supabase.q.dbPassword'),
|
|
1056
|
+
validate: (v) => (v && v.length >= 6 ? undefined : tr('new.supabase.q.dbPassword.required')),
|
|
1057
|
+
onCancel: cancel,
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
980
1060
|
ui.log.info(tr('new.internet.warning'));
|
|
981
1061
|
const createSpinner = ui.timedSpinner();
|
|
982
1062
|
createSpinner.start(tr('new.supabase.creating'));
|
|
@@ -1075,7 +1155,7 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1075
1155
|
|
|
1076
1156
|
// ── Firebase existing project: enable APIs + create Firestore/Storage ───
|
|
1077
1157
|
if (backend === 'firebase' && firebaseSetupMode === 'existing' && core.firebaseProjectId) {
|
|
1078
|
-
const ps4 = ui.
|
|
1158
|
+
const ps4 = ui.makeQuickStepper({ color: paintLime });
|
|
1079
1159
|
ps4.next(stepProgress('enable-apis', language));
|
|
1080
1160
|
const existingSetup = await setupExistingProject(core.firebaseProjectId, {
|
|
1081
1161
|
onProgress: (key, data) => {
|
|
@@ -1088,6 +1168,11 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1088
1168
|
ps4.next(stepProgress('firestore', language));
|
|
1089
1169
|
} else if (key === 'storage') {
|
|
1090
1170
|
ps4.next(stepProgress('storage', language));
|
|
1171
|
+
} else if (key === 'storage-cors') {
|
|
1172
|
+
ps4.next(stepProgress('storage-cors', language));
|
|
1173
|
+
} else if (key === 'storage-cors-warn') {
|
|
1174
|
+
ps4.stop();
|
|
1175
|
+
ui.log.warn(`Storage CORS: ${(data?.error || '').slice(0, 80)}\n${kleur.cyan(data?.url || '')}`);
|
|
1091
1176
|
} else if (key === 'auth-providers-warn') {
|
|
1092
1177
|
ps4.stop();
|
|
1093
1178
|
ui.log.warn(`${tr('new.firebase.interactive.authWarn')}\n${kleur.cyan(data?.url || '')}`);
|
|
@@ -1124,21 +1209,12 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1124
1209
|
// --with flag was passed: use those modules directly, skip preset prompt.
|
|
1125
1210
|
modules = preselectedModules;
|
|
1126
1211
|
} else if (isQuick) {
|
|
1127
|
-
// Quick mode:
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
options: [
|
|
1132
|
-
{ value: 'starter', label: tr('new.q.preset.starter') },
|
|
1133
|
-
{ value: 'saas', label: tr('new.q.preset.saas') },
|
|
1134
|
-
{ value: 'content', label: tr('new.q.preset.content') },
|
|
1135
|
-
{ value: 'full', label: tr('new.q.preset.full') },
|
|
1136
|
-
{ value: 'none', label: tr('new.q.preset.none') },
|
|
1137
|
-
],
|
|
1138
|
-
onCancel: cancel,
|
|
1139
|
-
});
|
|
1140
|
-
modules = MODULE_PRESETS[preset] || [];
|
|
1212
|
+
// Quick mode: ship all features by default. Facebook is excluded because
|
|
1213
|
+
// it requires App ID + token that we can't auto-generate — the user adds
|
|
1214
|
+
// it later with `kasy add facebook` when they have the credentials.
|
|
1215
|
+
modules = (MODULE_PRESETS.full || []).filter((m) => m !== 'facebook');
|
|
1141
1216
|
} else {
|
|
1217
|
+
section('new.advanced.section.features');
|
|
1142
1218
|
// Advanced mode: full multiselect — built from catalog, filtered by audience + backend.
|
|
1143
1219
|
const visibleFeatures = getVisibleFeatures({ audience: KASY_AUDIENCE, backend });
|
|
1144
1220
|
|
|
@@ -1174,10 +1250,15 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1174
1250
|
}
|
|
1175
1251
|
}
|
|
1176
1252
|
|
|
1253
|
+
// Pre-select everything available except Facebook — faster than ticking
|
|
1254
|
+
// 8 boxes; user just deselects what they don't want.
|
|
1255
|
+
const defaultSelected = moduleOptions
|
|
1256
|
+
.map((o) => o.value)
|
|
1257
|
+
.filter((id) => id !== 'facebook');
|
|
1177
1258
|
const rawModules = await ui.multiselect({
|
|
1178
1259
|
message: tr('new.firebase.q.modules'),
|
|
1179
1260
|
options: moduleOptions,
|
|
1180
|
-
initialValues:
|
|
1261
|
+
initialValues: defaultSelected,
|
|
1181
1262
|
required: false,
|
|
1182
1263
|
onCancel: cancel,
|
|
1183
1264
|
});
|
|
@@ -1188,79 +1269,105 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1188
1269
|
modules = [...new Set([...modules, 'web'])];
|
|
1189
1270
|
}
|
|
1190
1271
|
|
|
1272
|
+
// ── Credentials shortcut for Advanced ─────────────────────────────────────
|
|
1273
|
+
// Default: defer (no prompts). Matches Quick's "scaffold first, secrets later"
|
|
1274
|
+
// philosophy. User can opt-in to enter everything inline if they prefer.
|
|
1275
|
+
let configureCredsNow = false;
|
|
1276
|
+
if (!isQuick) {
|
|
1277
|
+
const FEATURES_WITH_CREDS = ['revenuecat', 'sentry', 'analytics', 'llm_chat', 'facebook'];
|
|
1278
|
+
if (modules.some((m) => FEATURES_WITH_CREDS.includes(m))) {
|
|
1279
|
+
section('new.advanced.section.creds');
|
|
1280
|
+
configureCredsNow = await ui.confirm({
|
|
1281
|
+
message: tr('new.advanced.q.configureCredsNow'),
|
|
1282
|
+
initialValue: false,
|
|
1283
|
+
onCancel: cancel,
|
|
1284
|
+
});
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
const askCreds = !isQuick && configureCredsNow;
|
|
1288
|
+
|
|
1191
1289
|
// ── Module-specific questions ───────────────────────────────────────────
|
|
1192
1290
|
const moduleAnswers = {};
|
|
1193
1291
|
|
|
1194
1292
|
if (modules.includes('revenuecat')) {
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1293
|
+
if (!askCreds) {
|
|
1294
|
+
// Defer mode: scaffold with empty keys + default paywall; user fills
|
|
1295
|
+
// them later via `kasy configure revenuecat`.
|
|
1296
|
+
moduleAnswers.rcTestKey = '';
|
|
1297
|
+
moduleAnswers.rcIosProdKey = '';
|
|
1298
|
+
moduleAnswers.rcAndroidProdKey = '';
|
|
1299
|
+
moduleAnswers.defaultPaywall = 'basic';
|
|
1300
|
+
} else {
|
|
1301
|
+
// Three keys, all optional, but we require at least one. The kasy run
|
|
1302
|
+
// command picks the right key based on the device:
|
|
1303
|
+
// simulator/emulator → RC_TEST_KEY
|
|
1304
|
+
// physical iOS → RC_IOS_PROD_KEY (falls back to test if missing)
|
|
1305
|
+
// physical Android → RC_ANDROID_PROD_KEY (falls back to test if missing)
|
|
1306
|
+
moduleAnswers.rcTestKey = ((await ui.text({
|
|
1307
|
+
message: tr('new.firebase.q.revenuecat.test'),
|
|
1308
|
+
validate: (v) => {
|
|
1309
|
+
const s = (v || '').trim();
|
|
1310
|
+
if (!s) return undefined;
|
|
1311
|
+
return /^test_/.test(s) ? undefined : tr('new.firebase.q.revenuecat.test.invalid');
|
|
1312
|
+
},
|
|
1313
|
+
onCancel: cancel,
|
|
1314
|
+
})) || '').trim();
|
|
1315
|
+
moduleAnswers.rcIosProdKey = ((await ui.text({
|
|
1316
|
+
message: tr('new.firebase.q.revenuecat.iosProd'),
|
|
1317
|
+
validate: (v) => {
|
|
1318
|
+
const s = (v || '').trim();
|
|
1319
|
+
if (!s) return undefined;
|
|
1320
|
+
return /^appl_/.test(s) ? undefined : tr('new.firebase.q.revenuecat.iosProd.invalid');
|
|
1321
|
+
},
|
|
1322
|
+
onCancel: cancel,
|
|
1323
|
+
})) || '').trim();
|
|
1324
|
+
moduleAnswers.rcAndroidProdKey = ((await ui.text({
|
|
1325
|
+
message: tr('new.firebase.q.revenuecat.androidProd'),
|
|
1326
|
+
validate: (v) => {
|
|
1327
|
+
const s = (v || '').trim();
|
|
1328
|
+
if (!s) return undefined;
|
|
1329
|
+
return /^goog_/.test(s) ? undefined : tr('new.firebase.q.revenuecat.androidProd.invalid');
|
|
1330
|
+
},
|
|
1331
|
+
onCancel: cancel,
|
|
1332
|
+
})) || '').trim();
|
|
1333
|
+
if (!moduleAnswers.rcTestKey && !moduleAnswers.rcIosProdKey && !moduleAnswers.rcAndroidProdKey) {
|
|
1334
|
+
// Non-blocking: user can fill .env later. Just warn so they're not surprised.
|
|
1335
|
+
ui.log.warn(tr('new.firebase.q.revenuecat.atLeastOne'));
|
|
1336
|
+
}
|
|
1337
|
+
moduleAnswers.defaultPaywall = await ui.select({
|
|
1338
|
+
message: tr('new.firebase.q.paywall'),
|
|
1339
|
+
initialValue: 'basic',
|
|
1340
|
+
options: [
|
|
1341
|
+
{ value: 'basic', label: 'Basic (list of plans)' },
|
|
1342
|
+
{ value: 'withSwitch', label: 'With trial switch' },
|
|
1343
|
+
{ value: 'basicRow', label: 'Row + comparison table' },
|
|
1344
|
+
{ value: 'minimal', label: 'Minimal (benefits + CTA)' },
|
|
1345
|
+
],
|
|
1346
|
+
onCancel: cancel,
|
|
1347
|
+
});
|
|
1230
1348
|
}
|
|
1231
|
-
moduleAnswers.defaultPaywall = await ui.select({
|
|
1232
|
-
message: tr('new.firebase.q.paywall'),
|
|
1233
|
-
initialValue: 'basic',
|
|
1234
|
-
options: [
|
|
1235
|
-
{ value: 'basic', label: 'Basic (list of plans)' },
|
|
1236
|
-
{ value: 'withSwitch', label: 'With trial switch' },
|
|
1237
|
-
{ value: 'basicRow', label: 'Row + comparison table' },
|
|
1238
|
-
{ value: 'minimal', label: 'Minimal (benefits + CTA)' },
|
|
1239
|
-
],
|
|
1240
|
-
onCancel: cancel,
|
|
1241
|
-
});
|
|
1242
1349
|
}
|
|
1243
1350
|
|
|
1244
|
-
// RC web key — only
|
|
1245
|
-
if (
|
|
1246
|
-
const rcWebKey = await ui.text({
|
|
1247
|
-
message: tr('new.firebase.q.revenuecat.webKey'),
|
|
1248
|
-
validate: (v) => {
|
|
1249
|
-
if (!v || !v.trim()) return undefined; // optional — blank is fine
|
|
1250
|
-
return /^rcb_/.test(v.trim()) ? undefined : tr('new.firebase.q.revenuecat.webKey.invalid');
|
|
1251
|
-
},
|
|
1252
|
-
onCancel: cancel,
|
|
1253
|
-
});
|
|
1351
|
+
// RC web key — only when configuring inline. Otherwise mark scaffolded.
|
|
1352
|
+
if (modules.includes('revenuecat') && modules.includes('web')) {
|
|
1254
1353
|
moduleAnswers.revenuecatWeb = true;
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1354
|
+
if (askCreds) {
|
|
1355
|
+
const rcWebKey = await ui.text({
|
|
1356
|
+
message: tr('new.firebase.q.revenuecat.webKey'),
|
|
1357
|
+
validate: (v) => {
|
|
1358
|
+
if (!v || !v.trim()) return undefined; // optional — blank is fine
|
|
1359
|
+
return /^rcb_/.test(v.trim()) ? undefined : tr('new.firebase.q.revenuecat.webKey.invalid');
|
|
1360
|
+
},
|
|
1361
|
+
onCancel: cancel,
|
|
1362
|
+
});
|
|
1363
|
+
moduleAnswers.rcWebKey = (rcWebKey || '').trim();
|
|
1364
|
+
} else {
|
|
1365
|
+
moduleAnswers.rcWebKey = '';
|
|
1366
|
+
}
|
|
1260
1367
|
}
|
|
1261
1368
|
|
|
1262
|
-
// Sentry DSN —
|
|
1263
|
-
if (
|
|
1369
|
+
// Sentry DSN — only when configuring inline.
|
|
1370
|
+
if (askCreds && modules.includes('sentry')) {
|
|
1264
1371
|
moduleAnswers.sentryDsn = await ui.text({
|
|
1265
1372
|
message: tr('new.firebase.q.sentry.dsn'),
|
|
1266
1373
|
validate: (v) => {
|
|
@@ -1273,23 +1380,17 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1273
1380
|
});
|
|
1274
1381
|
}
|
|
1275
1382
|
|
|
1276
|
-
// Mixpanel token —
|
|
1277
|
-
if (
|
|
1383
|
+
// Mixpanel token — only when configuring inline.
|
|
1384
|
+
if (askCreds && modules.includes('analytics')) {
|
|
1278
1385
|
moduleAnswers.mixpanelToken = await ui.text({
|
|
1279
1386
|
message: tr('new.firebase.q.mixpanel.token'),
|
|
1280
1387
|
onCancel: cancel,
|
|
1281
1388
|
});
|
|
1282
1389
|
}
|
|
1283
1390
|
|
|
1284
|
-
// LLM Chat credentials
|
|
1285
|
-
if (
|
|
1286
|
-
|
|
1287
|
-
message: tr('new.q.llm_chat.configureNow'),
|
|
1288
|
-
initialValue: true,
|
|
1289
|
-
onCancel: cancel,
|
|
1290
|
-
});
|
|
1291
|
-
|
|
1292
|
-
if (configureLlmNow) {
|
|
1391
|
+
// LLM Chat credentials.
|
|
1392
|
+
if (modules.includes('llm_chat')) {
|
|
1393
|
+
if (askCreds) {
|
|
1293
1394
|
moduleAnswers.llmProvider = await ui.select({
|
|
1294
1395
|
message: tr('add.prompt.llmProvider'),
|
|
1295
1396
|
initialValue: 'openai',
|
|
@@ -1310,36 +1411,33 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1310
1411
|
} else {
|
|
1311
1412
|
moduleAnswers.llmConfigureLater = true;
|
|
1312
1413
|
}
|
|
1313
|
-
} else if (isQuick && modules.includes('llm_chat')) {
|
|
1314
|
-
moduleAnswers.llmConfigureLater = true;
|
|
1315
1414
|
}
|
|
1316
1415
|
|
|
1317
|
-
// Facebook — required credentials
|
|
1416
|
+
// Facebook — required credentials when inline; scaffold-only otherwise.
|
|
1318
1417
|
if (modules.includes('facebook')) {
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1418
|
+
if (askCreds) {
|
|
1419
|
+
moduleAnswers.fbAppId = await ui.text({
|
|
1420
|
+
message: tr('new.firebase.q.facebook.appId'),
|
|
1421
|
+
validate: (v) => {
|
|
1422
|
+
if (!v || !v.trim()) return tr('new.firebase.q.facebook.appId.required');
|
|
1423
|
+
return /^\d+$/.test(v.trim()) ? undefined : tr('new.firebase.q.facebook.appId.invalid');
|
|
1424
|
+
},
|
|
1425
|
+
onCancel: cancel,
|
|
1426
|
+
});
|
|
1427
|
+
moduleAnswers.fbToken = await ui.password({
|
|
1428
|
+
message: tr('new.firebase.q.facebook.token'),
|
|
1429
|
+
validate: (v) => (v && v.trim() ? undefined : tr('new.firebase.q.facebook.token.required')),
|
|
1430
|
+
onCancel: cancel,
|
|
1431
|
+
});
|
|
1432
|
+
} else {
|
|
1433
|
+
moduleAnswers.fbAppId = '';
|
|
1434
|
+
moduleAnswers.fbToken = '';
|
|
1435
|
+
}
|
|
1332
1436
|
}
|
|
1333
1437
|
|
|
1334
|
-
// Server secrets (webhook, Meta Ads) —
|
|
1335
|
-
if (
|
|
1336
|
-
|
|
1337
|
-
message: tr('new.firebase.q.secrets.configureNow'),
|
|
1338
|
-
initialValue: true,
|
|
1339
|
-
onCancel: cancel,
|
|
1340
|
-
});
|
|
1341
|
-
|
|
1342
|
-
if (configureSecretsNow) {
|
|
1438
|
+
// Server secrets (webhook, Meta Ads) — only when configuring inline.
|
|
1439
|
+
if (modules.includes('revenuecat') && (backend === 'supabase' || backend === 'firebase')) {
|
|
1440
|
+
if (askCreds) {
|
|
1343
1441
|
moduleAnswers.rcWebhookKey = await ui.text({
|
|
1344
1442
|
message: tr('new.firebase.q.revenuecat.webhookKey'),
|
|
1345
1443
|
initialValue: generateWebhookKey(),
|
|
@@ -1361,9 +1459,6 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1361
1459
|
} else {
|
|
1362
1460
|
moduleAnswers.secretsConfigureLater = true;
|
|
1363
1461
|
}
|
|
1364
|
-
} else if (isQuick && modules.includes('revenuecat')) {
|
|
1365
|
-
// Quick mode: always defer secrets to `kasy deploy`.
|
|
1366
|
-
moduleAnswers.secretsConfigureLater = true;
|
|
1367
1462
|
}
|
|
1368
1463
|
|
|
1369
1464
|
// ── Deploy is now a separate command (`kasy deploy`) ─────────────
|
|
@@ -1384,25 +1479,13 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1384
1479
|
apiBaseUrl: core.apiBaseUrl?.trim(),
|
|
1385
1480
|
};
|
|
1386
1481
|
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
const proceed = await ui.confirm({
|
|
1391
|
-
message: tr('new.firebase.confirm.proceed'),
|
|
1392
|
-
initialValue: true,
|
|
1393
|
-
onCancel: cancel,
|
|
1394
|
-
});
|
|
1395
|
-
if (!proceed) {
|
|
1396
|
-
ui.cancel(tr('prompt.cancelled'));
|
|
1397
|
-
process.exit(0);
|
|
1398
|
-
}
|
|
1399
|
-
}
|
|
1482
|
+
// No mid-flow confirmation: both Quick and Advanced go straight to the
|
|
1483
|
+
// generation step once choices are made. The success card at the end shows
|
|
1484
|
+
// the full summary. Cancel any time with Ctrl+C.
|
|
1400
1485
|
|
|
1401
1486
|
// ── Generate ────────────────────────────────────────────────────────────
|
|
1402
|
-
//
|
|
1403
|
-
|
|
1404
|
-
// spinner with a mutating message.
|
|
1405
|
-
const stepper = ui.makeTimedStepper();
|
|
1487
|
+
// Quick: single rolling line that mutates message. Advanced: stack each step.
|
|
1488
|
+
const stepper = ui.makeQuickStepper({ color: paintLime });
|
|
1406
1489
|
// First step started here so even silent prep work shows progress.
|
|
1407
1490
|
stepper.next(stepProgress('project-setup', language));
|
|
1408
1491
|
|
|
@@ -1448,6 +1531,10 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1448
1531
|
functionsRegion: firebaseRegion,
|
|
1449
1532
|
language,
|
|
1450
1533
|
onProgress,
|
|
1534
|
+
// Google Sign-In is enabled in a later step via Firebase CLI, which is
|
|
1535
|
+
// when the OAuth Web Client + REVERSED_CLIENT_ID get created. Defer the
|
|
1536
|
+
// two patches that depend on those IDs so they don't fail noisily here.
|
|
1537
|
+
deferGoogleAuthPatches: true,
|
|
1451
1538
|
});
|
|
1452
1539
|
}
|
|
1453
1540
|
// Close the last in-flight step. We don't need a label — its own message
|
|
@@ -1507,7 +1594,13 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1507
1594
|
// only way to make the client secret available via the Identity Toolkit API.
|
|
1508
1595
|
// Non-fatal: silently ignored if it fails (e.g. API not yet propagated).
|
|
1509
1596
|
if (answers.firebaseProjectId) {
|
|
1510
|
-
await enableAuthProviders(answers.firebaseProjectId);
|
|
1597
|
+
const authResult = await enableAuthProviders(answers.firebaseProjectId);
|
|
1598
|
+
if (authResult.ok && !authResult.googleSignInSkipped) {
|
|
1599
|
+
printStepResult({ name: 'google sign-in', ok: true }, language);
|
|
1600
|
+
}
|
|
1601
|
+
if (authResult.appleEnabled) {
|
|
1602
|
+
printStepResult({ name: 'apple sign-in', ok: true }, language);
|
|
1603
|
+
}
|
|
1511
1604
|
}
|
|
1512
1605
|
|
|
1513
1606
|
// Try to get the Client Secret from Identity Toolkit API (requires gcloud auth)
|
|
@@ -1571,6 +1664,31 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1571
1664
|
setupSpinner.error(err.message);
|
|
1572
1665
|
ui.log.warn(tr('new.supabase.setupManual'));
|
|
1573
1666
|
}
|
|
1667
|
+
|
|
1668
|
+
// Quick mode generates the DB password automatically — persist it so the
|
|
1669
|
+
// user can recover it later (e.g. to log into Supabase Studio).
|
|
1670
|
+
if (isQuick && supabaseCreate && supabaseDbPassword) {
|
|
1671
|
+
try {
|
|
1672
|
+
const kasyDir = path.join(targetDir, '.kasy');
|
|
1673
|
+
await fs.ensureDir(kasyDir);
|
|
1674
|
+
await fs.writeJson(
|
|
1675
|
+
path.join(kasyDir, 'supabase.json'),
|
|
1676
|
+
{ projectRef: supabaseSetupPayload.projectRef, dbPassword: supabaseDbPassword },
|
|
1677
|
+
{ spaces: 2 }
|
|
1678
|
+
);
|
|
1679
|
+
const gitignorePath = path.join(targetDir, '.gitignore');
|
|
1680
|
+
const gitignoreEntry = '.kasy/supabase.json';
|
|
1681
|
+
if (await fs.pathExists(gitignorePath)) {
|
|
1682
|
+
const existing = await fs.readFile(gitignorePath, 'utf8');
|
|
1683
|
+
if (!existing.includes(gitignoreEntry)) {
|
|
1684
|
+
await fs.appendFile(gitignorePath, `\n# Supabase auto-generated DB password (credentials — do not commit)\n${gitignoreEntry}\n`, 'utf8');
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
ui.log.info(tr('new.supabase.passwordSaved'));
|
|
1688
|
+
} catch (_) {
|
|
1689
|
+
// Non-fatal: user can rotate password via Supabase dashboard if needed.
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1574
1692
|
}
|
|
1575
1693
|
}
|
|
1576
1694
|
|
|
@@ -1640,16 +1758,57 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1640
1758
|
);
|
|
1641
1759
|
}
|
|
1642
1760
|
|
|
1643
|
-
// ──
|
|
1644
|
-
//
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1761
|
+
// ── Google + Apple Sign-In: use Firebase CLI for Google (creates OAuth client),
|
|
1762
|
+
// then REST API for Apple as best-effort. ───────────────────────────────────
|
|
1763
|
+
// Firebase CLI's `deploy --only auth` is the only documented path that auto-
|
|
1764
|
+
// creates the OAuth 2.0 Web Client without manual Console clicks — the same
|
|
1765
|
+
// backend that the Console hits internally.
|
|
1766
|
+
// (Supabase backend keeps the REST-only retry — its OAuth client is provisioned
|
|
1767
|
+
// differently via the linked Firebase Web app.)
|
|
1768
|
+
if (backend === 'firebase' && answers.firebaseProjectId && flutterfireStep?.ok) {
|
|
1769
|
+
const googleSpinner = ui.spinner();
|
|
1770
|
+
googleSpinner.start(tr('new.google.enabling'));
|
|
1771
|
+
const cliResult = await enableAuthViaFirebaseCli({
|
|
1772
|
+
projectDir: targetDir,
|
|
1773
|
+
projectId: answers.firebaseProjectId,
|
|
1774
|
+
appName: answers.appName,
|
|
1775
|
+
});
|
|
1776
|
+
googleSpinner.stop(tr('new.google.enabling'));
|
|
1777
|
+
if (cliResult.ok) {
|
|
1778
|
+
printStepResult({ name: 'google sign-in', ok: true }, language);
|
|
1779
|
+
|
|
1780
|
+
// Google deploy created the OAuth Web Client + iOS client. Re-run flutterfire
|
|
1781
|
+
// so google-services.json and GoogleService-Info.plist pick up the new IDs,
|
|
1782
|
+
// then re-apply the two patches that failed during the initial pass.
|
|
1783
|
+
const rerunSpinner = ui.spinner();
|
|
1784
|
+
rerunSpinner.start(tr('new.google.refreshConfigs'));
|
|
1785
|
+
const ffRerun = await flutterfireConfigure(targetDir, answers.firebaseProjectId, {
|
|
1786
|
+
includeWeb: answers.includeWeb !== false,
|
|
1787
|
+
});
|
|
1788
|
+
rerunSpinner.stop(tr('new.google.refreshConfigs'));
|
|
1789
|
+
if (ffRerun.ok) {
|
|
1790
|
+
const gaResult = await writeGoogleAuthOptions(targetDir);
|
|
1791
|
+
printStepResult({ name: 'google-auth-options', ok: gaResult.ok, detail: gaResult.error }, language);
|
|
1792
|
+
const iosResult = await writeGoogleIosUrlScheme(targetDir);
|
|
1793
|
+
printStepResult({ name: 'google-ios-url-scheme', ok: iosResult.ok, detail: iosResult.error }, language);
|
|
1794
|
+
}
|
|
1795
|
+
} else {
|
|
1796
|
+
const authUrl = `https://console.firebase.google.com/project/${answers.firebaseProjectId}/authentication/providers`;
|
|
1797
|
+
const reason = cliResult.error === 'support_email_required'
|
|
1798
|
+
? tr('new.google.manualHint.noEmail')
|
|
1799
|
+
: tr('new.google.manualHint');
|
|
1800
|
+
ui.log.warn(`${reason}\n${kleur.cyan(authUrl)}`);
|
|
1801
|
+
}
|
|
1802
|
+
// Apple Sign-In remains a best-effort REST POST (no CLI support yet).
|
|
1803
|
+
const appleResult = await enableAuthProviders(answers.firebaseProjectId);
|
|
1804
|
+
if (appleResult.appleEnabled) {
|
|
1805
|
+
printStepResult({ name: 'apple sign-in', ok: true }, language);
|
|
1806
|
+
}
|
|
1651
1807
|
}
|
|
1652
1808
|
|
|
1809
|
+
// APNs key (iOS push) is intentionally not mentioned here — it only becomes
|
|
1810
|
+
// relevant when shipping to iOS, and the docs at kasy.dev/docs/apns explain it.
|
|
1811
|
+
|
|
1653
1812
|
printSuccessCard(tr, answers, targetDir);
|
|
1654
1813
|
|
|
1655
1814
|
ui.outro(kleur.bold(tr('new.success.title')));
|