kasy-cli 1.17.0 → 1.18.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 +15 -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 +403 -238
- package/lib/commands/run.js +1 -1
- 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 +44 -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 +58 -5
- package/lib/utils/i18n/messages-es.js +58 -5
- package/lib/utils/i18n/messages-pt.js +59 -6
- package/lib/utils/ui.js +79 -4
- package/package.json +1 -1
- 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 +834 -0
- package/templates/firebase/lib/components/kasy_tabs.dart +145 -61
- package/templates/firebase/lib/core/home_widgets/home_widget_mywidget_service.dart +12 -18
- package/templates/firebase/lib/features/home/home_components_preview_registry.dart +565 -77
- 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/pubspec.yaml +1 -1
- package/templates/firebase/web/index.html +9 -0
- 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/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, infoBox, successBox } = require('../utils/brand');
|
|
32
|
+
const { printBanner, infoBox, 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,
|
|
@@ -213,6 +212,7 @@ const STEP_LABELS = {
|
|
|
213
212
|
'pub-get': { en: 'Packages installed', pt: 'Pacotes instalados', es: 'Paquetes instalados' },
|
|
214
213
|
'slang': { en: 'Translations generated', pt: 'Traducoes geradas', es: 'Traducciones generadas' },
|
|
215
214
|
'build-runner': { en: 'Code generated (Riverpod / Freezed)', pt: 'Codigo gerado (Riverpod / Freezed)', es: 'Codigo generado (Riverpod / Freezed)' },
|
|
215
|
+
'dart-fix': { en: 'Lints auto-fixed', pt: 'Avisos de estilo corrigidos', es: 'Lints corregidos automaticamente' },
|
|
216
216
|
'flutterfire': { en: 'Firebase configured (flutterfire)', pt: 'Firebase configurado (flutterfire)', es: 'Firebase configurado (flutterfire)' },
|
|
217
217
|
'Service Account key': { en: 'Service Account key', pt: 'Chave de servico', es: 'Clave de servicio' },
|
|
218
218
|
'firebase_key.json': { en: 'Service account key copied', pt: 'Chave de conta de servico copiada', es: 'Clave de cuenta de servicio copiada' },
|
|
@@ -232,6 +232,7 @@ const STEP_LABELS = {
|
|
|
232
232
|
'supabase db push': { en: 'Database migrated', pt: 'Banco migrado', es: 'Base de datos migrada' },
|
|
233
233
|
'anonymous sign-in': { en: 'Anonymous sign-in enabled', pt: 'Login anonimo ativado', es: 'Inicio de sesion anonimo activado' },
|
|
234
234
|
'google sign-in': { en: 'Google Sign-In enabled', pt: 'Google Sign-In ativado', es: 'Google Sign-In activado' },
|
|
235
|
+
'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
236
|
'google-auth-options': { en: 'Google client IDs written', pt: 'Client IDs do Google gravados', es: 'Client IDs de Google escritos' },
|
|
236
237
|
'ios-url-scheme': { en: 'iOS URL scheme registered', pt: 'URL scheme iOS registrado', es: 'URL scheme iOS registrado' },
|
|
237
238
|
'google-ios-url-scheme': { en: 'iOS Google URL scheme registered', pt: 'URL scheme Google iOS registrado', es: 'URL scheme Google iOS registrado' },
|
|
@@ -260,6 +261,7 @@ const STEP_PROGRESS = {
|
|
|
260
261
|
'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
262
|
'slang': { en: 'Generating translations…', pt: 'Gerando traducoes…', es: 'Generando traducciones…' },
|
|
262
263
|
'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)' },
|
|
264
|
+
'dart-fix': { en: 'Auto-fixing lints…', pt: 'Corrigindo avisos de estilo…', es: 'Corrigiendo lints automaticamente…' },
|
|
263
265
|
'flutterfire': { en: 'Connecting to Firebase…', pt: 'Conectando ao Firebase…', es: 'Conectando a Firebase…' },
|
|
264
266
|
'deploy': { en: 'Deploying backend to Firebase…', pt: 'Publicando backend no Firebase…', es: 'Desplegando backend en Firebase…' },
|
|
265
267
|
'gcp-project': { en: 'Creating GCP project…', pt: 'Criando projeto GCP…', es: 'Creando proyecto GCP…' },
|
|
@@ -324,32 +326,46 @@ function printCreateFromScratchStatus(result, tr) {
|
|
|
324
326
|
|
|
325
327
|
function printSuccessCard(tr, answers, targetDir) {
|
|
326
328
|
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
329
|
|
|
333
330
|
const lines = [];
|
|
331
|
+
|
|
332
|
+
if (answers.modules?.length > 0) {
|
|
333
|
+
const byId = Object.fromEntries(FEATURE_CATALOG.map((f) => [f.id, f]));
|
|
334
|
+
const visible = answers.modules.filter((id) => byId[id]);
|
|
335
|
+
if (visible.length > 0) {
|
|
336
|
+
lines.push(paintLime(`✓ ${tr('new.success.featuresInstalled')}`));
|
|
337
|
+
for (const id of visible) {
|
|
338
|
+
const name = byId[id]?.displayName || id;
|
|
339
|
+
lines.push(` ${kleur.dim('•')} ${name}`);
|
|
340
|
+
}
|
|
341
|
+
lines.push('');
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
334
345
|
lines.push(kleur.bold(tr('new.success.nextSteps')));
|
|
335
346
|
lines.push('');
|
|
336
|
-
|
|
347
|
+
|
|
348
|
+
let stepNum = 1;
|
|
349
|
+
lines.push(`${kleur.dim(`${stepNum++}.`)} ${kleur.dim(tr('new.success.step.cd'))}`);
|
|
337
350
|
lines.push(` ${kleur.cyan(`cd ${folderName}`)}`);
|
|
338
|
-
|
|
351
|
+
lines.push('');
|
|
352
|
+
|
|
339
353
|
if (answers.backend === 'firebase') {
|
|
340
|
-
lines.push('');
|
|
341
354
|
lines.push(`${kleur.dim(`${stepNum++}.`)} ${kleur.dim(tr('new.success.step.deploy'))}`);
|
|
342
355
|
lines.push(` ${kleur.cyan('kasy deploy')}`);
|
|
356
|
+
lines.push('');
|
|
343
357
|
}
|
|
358
|
+
|
|
359
|
+
lines.push(`${kleur.dim(`${stepNum++}.`)} ${kleur.dim(tr('new.success.step.configure'))}`);
|
|
360
|
+
lines.push(` ${kleur.cyan('kasy configure')}`);
|
|
344
361
|
lines.push('');
|
|
362
|
+
|
|
345
363
|
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
|
-
}
|
|
364
|
+
lines.push(` ${kleur.cyan('kasy run')} ${kleur.dim(tr('new.success.step.run.vscode'))}`);
|
|
365
|
+
lines.push('');
|
|
366
|
+
|
|
367
|
+
lines.push(`${kleur.dim(`${stepNum++}.`)} ${kleur.dim(tr('new.success.step.docs'))}`);
|
|
368
|
+
lines.push(` ${kleur.cyan('https://kasy.dev/docs')}`);
|
|
353
369
|
|
|
354
370
|
console.log(successBox(`🎉 ${tr('new.success.title')}`, lines.join('\n')));
|
|
355
371
|
}
|
|
@@ -464,9 +480,9 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
464
480
|
message: tr('new.q.backend'),
|
|
465
481
|
initialValue: 'firebase',
|
|
466
482
|
options: [
|
|
467
|
-
{ value: 'firebase', label: '🔥 Firebase'
|
|
468
|
-
{ value: 'supabase', label: '🟢 Supabase'
|
|
469
|
-
{ value: 'api', label: '🔗 API REST'
|
|
483
|
+
{ value: 'firebase', label: '🔥 Firebase' },
|
|
484
|
+
{ value: 'supabase', label: '🟢 Supabase' },
|
|
485
|
+
{ value: 'api', label: '🔗 API REST' },
|
|
470
486
|
],
|
|
471
487
|
onCancel: cancel,
|
|
472
488
|
});
|
|
@@ -504,59 +520,38 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
504
520
|
}
|
|
505
521
|
}
|
|
506
522
|
|
|
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
|
|
523
|
+
// Helper: slug → bundle id (e.g. "MeuApp" → "com.meuapp.app")
|
|
524
|
+
const deriveBundleId = (name) => {
|
|
525
|
+
const trimmed = (name || '').trim();
|
|
526
|
+
if (!trimmed) return 'com.example.app';
|
|
527
|
+
const slug = trimmed
|
|
517
528
|
.normalize('NFD')
|
|
518
529
|
.replace(/[̀-ͯ]/g, '')
|
|
519
530
|
.toLowerCase()
|
|
520
531
|
.replace(/[^a-z0-9]/g, '');
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
532
|
+
if (!slug || /^\d/.test(slug)) return 'com.example.app';
|
|
533
|
+
return `com.${slug}.app`;
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
// ── App name — derived from argv when given, otherwise asked once ──────────
|
|
537
|
+
let core;
|
|
538
|
+
if (hasExplicitDir) {
|
|
539
|
+
// `kasy new MeuApp` — use the argument as the name; don't ask again.
|
|
540
|
+
const appName = path.basename(targetDir);
|
|
541
|
+
core = { appName, bundleId: deriveBundleId(appName) };
|
|
542
|
+
ui.log.info(`App: ${kleur.white(core.appName)}`);
|
|
543
|
+
} else if (yes) {
|
|
544
|
+
ui.log.error('--yes requires an app name: kasy new MyApp --yes');
|
|
545
|
+
ui.cancel(tr('new.firebase.error.aborted'));
|
|
546
|
+
process.exit(1);
|
|
525
547
|
} else {
|
|
526
548
|
const appName = await ui.text({
|
|
527
549
|
message: tr('new.firebase.q.appName'),
|
|
528
550
|
placeholder: tr('new.firebase.q.appName.hint'),
|
|
529
|
-
initialValue: hasExplicitDir ? path.basename(targetDir) : '',
|
|
530
551
|
validate: (v) => (v && v.trim() ? undefined : tr('new.firebase.q.appName.required')),
|
|
531
552
|
onCancel: cancel,
|
|
532
553
|
});
|
|
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 };
|
|
554
|
+
core = { appName, bundleId: deriveBundleId(appName) };
|
|
560
555
|
}
|
|
561
556
|
|
|
562
557
|
// Resolve targetDir now that we have the app name
|
|
@@ -571,10 +566,93 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
571
566
|
}
|
|
572
567
|
}
|
|
573
568
|
|
|
574
|
-
// ──
|
|
575
|
-
|
|
576
|
-
let firebaseSetupMode = 'existing';
|
|
569
|
+
// ── Wizard mode — Quick (zero config, recommended) or Step-by-step (all options) ─
|
|
570
|
+
let isQuick = yes; // --yes implies Quick mode
|
|
577
571
|
if (!yes) {
|
|
572
|
+
const wizardMode = await ui.select({
|
|
573
|
+
message: tr('new.q.mode'),
|
|
574
|
+
initialValue: 'quick',
|
|
575
|
+
options: [
|
|
576
|
+
{ value: 'quick', label: tr('new.q.mode.quick') },
|
|
577
|
+
{ value: 'advanced', label: tr('new.q.mode.advanced') },
|
|
578
|
+
],
|
|
579
|
+
onCancel: cancel,
|
|
580
|
+
});
|
|
581
|
+
isQuick = wizardMode === 'quick';
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// ── gcloud is required for Quick mode (creates GCP project + enables Auth via API) ─
|
|
585
|
+
// Failing here, before the project is generated, is much friendlier than dying mid-flow.
|
|
586
|
+
if (isQuick) {
|
|
587
|
+
const gcloudCheck = await checkGcloudAuth();
|
|
588
|
+
if (!gcloudCheck.ok) {
|
|
589
|
+
ui.log.error(tr('new.firebase.create.gcloudRequired'));
|
|
590
|
+
if (gcloudCheck.missing === 'gcloud') {
|
|
591
|
+
const instructions = getGcloudInstallInstructions();
|
|
592
|
+
const noteLines = [tr('new.firebase.create.installTitle')];
|
|
593
|
+
if (instructions.install) noteLines.push(`${tr('new.firebase.create.installCommand')}:\n ${kleur.cyan(instructions.install)}`);
|
|
594
|
+
if (instructions.hint) noteLines.push(instructions.hint);
|
|
595
|
+
noteLines.push(`${tr('new.firebase.create.installAfter')}:\n ${kleur.cyan(instructions.after)}`);
|
|
596
|
+
noteLines.push(`${tr('new.firebase.create.installUrl')}: ${instructions.url}`);
|
|
597
|
+
ui.note(noteLines.join('\n\n'));
|
|
598
|
+
} else {
|
|
599
|
+
ui.note(tr('new.firebase.create.authCommand'));
|
|
600
|
+
}
|
|
601
|
+
ui.cancel(tr('new.firebase.error.aborted'));
|
|
602
|
+
process.exit(1);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// ── Billing account check — Firebase needs an active billing account (Blaze) ─
|
|
606
|
+
// Without it, project creation succeeds but Storage and Cloud Functions fail later.
|
|
607
|
+
// Catching it here saves the user from a confusing mid-flow error.
|
|
608
|
+
const ensureBilling = async () => {
|
|
609
|
+
const billing = await listBillingAccounts();
|
|
610
|
+
if (billing.ok && billing.accounts?.length > 0) return true;
|
|
611
|
+
const billingUrl = 'https://console.cloud.google.com/billing/create';
|
|
612
|
+
ui.log.warn(tr('new.firebase.billing.required'));
|
|
613
|
+
ui.note(`${tr('new.firebase.billing.create.steps')}\n${kleur.cyan(billingUrl)}`);
|
|
614
|
+
openUrl(billingUrl);
|
|
615
|
+
const ready = await ui.confirm({
|
|
616
|
+
message: tr('new.firebase.billing.created.ready'),
|
|
617
|
+
initialValue: true,
|
|
618
|
+
onCancel: cancel,
|
|
619
|
+
});
|
|
620
|
+
if (!ready) {
|
|
621
|
+
ui.cancel(tr('new.firebase.error.aborted'));
|
|
622
|
+
process.exit(0);
|
|
623
|
+
}
|
|
624
|
+
const recheck = await listBillingAccounts();
|
|
625
|
+
if (!recheck.ok || !recheck.accounts?.length) {
|
|
626
|
+
ui.log.error(tr('new.firebase.billing.stillMissing'));
|
|
627
|
+
ui.cancel(tr('new.firebase.error.aborted'));
|
|
628
|
+
process.exit(1);
|
|
629
|
+
}
|
|
630
|
+
return true;
|
|
631
|
+
};
|
|
632
|
+
await ensureBilling();
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// ── Bundle ID — Quick uses derived value silently; Step-by-step lets user override ─
|
|
636
|
+
if (!isQuick) {
|
|
637
|
+
const bundleId = await ui.text({
|
|
638
|
+
message: tr('new.firebase.q.bundleId'),
|
|
639
|
+
placeholder: tr('new.firebase.q.bundleId.hint'),
|
|
640
|
+
initialValue: core.bundleId,
|
|
641
|
+
validate: (v) => {
|
|
642
|
+
if (!v || !v.trim()) return tr('new.firebase.q.bundleId.required');
|
|
643
|
+
return /^[a-zA-Z][\w]*(\.[a-zA-Z][\w]*)+$/.test(v.trim())
|
|
644
|
+
? undefined
|
|
645
|
+
: tr('new.firebase.q.bundleId.invalid');
|
|
646
|
+
},
|
|
647
|
+
onCancel: cancel,
|
|
648
|
+
});
|
|
649
|
+
core.bundleId = bundleId;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// ── Firebase setup mode — Quick always creates a new project ───────────────
|
|
653
|
+
// (--yes implies Quick, so it also defaults to creating a fresh project.)
|
|
654
|
+
let firebaseSetupMode = 'create';
|
|
655
|
+
if (!isQuick) {
|
|
578
656
|
if (backend === 'firebase') {
|
|
579
657
|
firebaseSetupMode = await ui.select({
|
|
580
658
|
message: tr('new.firebase.q.setupMode'),
|
|
@@ -599,25 +677,13 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
599
677
|
}
|
|
600
678
|
}
|
|
601
679
|
|
|
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';
|
|
680
|
+
// ── Backend-specific prerequisites — only shown in Step-by-step mode ──────
|
|
681
|
+
// Quick mode hides this list: it intimidates beginners and the CLI handles
|
|
682
|
+
// most prerequisites automatically (gcloud check below, billing prompt, etc.).
|
|
683
|
+
if (!isQuick) {
|
|
684
|
+
printPrerequisites(tr, backend, firebaseSetupMode, backendCheckResults);
|
|
616
685
|
}
|
|
617
686
|
|
|
618
|
-
// ── Backend-specific prerequisites (dynamic: only show what's not yet verified) ─
|
|
619
|
-
printPrerequisites(tr, backend, firebaseSetupMode, backendCheckResults);
|
|
620
|
-
|
|
621
687
|
// ── Firebase project ID (if using an existing project) ──────────────────────
|
|
622
688
|
if (!yes) {
|
|
623
689
|
const needFirebaseProjectId =
|
|
@@ -639,19 +705,12 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
639
705
|
core.firebaseProjectId = firebaseProjectId;
|
|
640
706
|
}
|
|
641
707
|
} 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)}`);
|
|
708
|
+
// --yes mode: if --project was passed, reuse that existing project.
|
|
709
|
+
// Otherwise stay in 'create' mode and let setupFromScratch handle creation silently.
|
|
710
|
+
if (projectHint?.trim()) {
|
|
711
|
+
firebaseSetupMode = 'existing';
|
|
712
|
+
core.firebaseProjectId = projectHint.trim();
|
|
713
|
+
ui.log.info(`Project: ${kleur.white(core.firebaseProjectId)}`);
|
|
655
714
|
}
|
|
656
715
|
}
|
|
657
716
|
|
|
@@ -674,11 +733,13 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
674
733
|
// ── Firebase: create from scratch (when selected) ─────────────────────────
|
|
675
734
|
let firebaseIncludeWeb = true;
|
|
676
735
|
if (backend === 'firebase' && firebaseSetupMode === 'create') {
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
736
|
+
if (!isQuick) {
|
|
737
|
+
firebaseIncludeWeb = await ui.confirm({
|
|
738
|
+
message: tr('new.firebase.create.includeWeb'),
|
|
739
|
+
initialValue: true,
|
|
740
|
+
onCancel: cancel,
|
|
741
|
+
});
|
|
742
|
+
}
|
|
682
743
|
const gcloudCheck = await checkGcloudAuth();
|
|
683
744
|
if (!gcloudCheck.ok) {
|
|
684
745
|
ui.log.error(tr('new.firebase.create.gcloudRequired'));
|
|
@@ -701,30 +762,50 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
701
762
|
onCancel: cancel,
|
|
702
763
|
});
|
|
703
764
|
} else {
|
|
765
|
+
// Warn about duration + network before the org/billing prompts so the user
|
|
766
|
+
// can step away (or check the wifi) before the long-running call starts.
|
|
767
|
+
ui.log.info(tr('new.firebase.create.estimatedTime'));
|
|
704
768
|
const selectedOrgId = await promptOrganizationIfNeeded(tr, cancel);
|
|
705
769
|
let selectedBillingId = await promptBillingAccountIfNeeded(tr, cancel);
|
|
706
|
-
ui.
|
|
707
|
-
const ps1 = ui.makeTimedStepper();
|
|
770
|
+
const ps1 = isQuick ? ui.makeQuickStepper({ color: paintLime }) : ui.makeTimedStepper();
|
|
708
771
|
ps1.next(tr('new.firebase.create.creating'));
|
|
709
|
-
const
|
|
772
|
+
const ps1OnProgress = (key, data) => {
|
|
773
|
+
if (key === 'wait-propagate') {
|
|
774
|
+
ps1.next(tr('new.firebase.create.waitPropagate'));
|
|
775
|
+
} else if (key === 'firestore') {
|
|
776
|
+
ps1.next(stepProgress('firestore', language));
|
|
777
|
+
} else if (key === 'storage') {
|
|
778
|
+
ps1.next(stepProgress('storage', language));
|
|
779
|
+
} else if (key === 'auth-providers-warn') {
|
|
780
|
+
ps1.stop();
|
|
781
|
+
ui.log.warn(`${tr('new.firebase.interactive.authWarn')}\n${kleur.cyan(data?.url || '')}`);
|
|
782
|
+
}
|
|
783
|
+
};
|
|
784
|
+
let setupResult = await setupFromScratch(core.appName.trim(), core.bundleId.trim(), {
|
|
710
785
|
includeWeb: firebaseIncludeWeb,
|
|
711
786
|
region: firebaseRegion,
|
|
712
787
|
tr,
|
|
713
788
|
billingAccountId: selectedBillingId || undefined,
|
|
714
789
|
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
|
-
},
|
|
790
|
+
onProgress: ps1OnProgress,
|
|
727
791
|
});
|
|
792
|
+
|
|
793
|
+
// Silent retry for transient billing-quota errors (propagation delay,
|
|
794
|
+
// race between createProject and linkBilling). Keeps the same spinner
|
|
795
|
+
// line so the user doesn't see the red error if it succeeds on retry.
|
|
796
|
+
if (!setupResult.ok && setupResult.billingFailed && setupResult.billingQuotaError && setupResult.projectId) {
|
|
797
|
+
ps1.update(tr('new.firebase.create.billingWait'));
|
|
798
|
+
await new Promise((r) => setTimeout(r, 15000));
|
|
799
|
+
setupResult = await setupFromScratch(core.appName.trim(), core.bundleId.trim(), {
|
|
800
|
+
includeWeb: firebaseIncludeWeb,
|
|
801
|
+
region: firebaseRegion,
|
|
802
|
+
tr,
|
|
803
|
+
resumeFromBilling: { projectId: setupResult.projectId },
|
|
804
|
+
billingAccountId: selectedBillingId || undefined,
|
|
805
|
+
organizationId: selectedOrgId || undefined,
|
|
806
|
+
onProgress: ps1OnProgress,
|
|
807
|
+
});
|
|
808
|
+
}
|
|
728
809
|
const askReady = async (readyKey) => {
|
|
729
810
|
const ok = await ui.confirm({
|
|
730
811
|
message: tr(readyKey),
|
|
@@ -748,26 +829,25 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
748
829
|
ui.log.info(`Project ID: ${core.firebaseProjectId}`);
|
|
749
830
|
printCreateFromScratchStatus(setupResult, tr);
|
|
750
831
|
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);
|
|
832
|
+
// Stay silent when Google is the only thing missing — the post-flutterfire
|
|
833
|
+
// retry will activate it once the OAuth Web Client exists. We only warn
|
|
834
|
+
// here when Email/Anonymous themselves failed (which is the rare path).
|
|
835
|
+
if (!setupResult.authEnabled) {
|
|
836
|
+
showBeforeContinue('new.firebase.create.beforeContinue.step1.noAuth', authUrl);
|
|
762
837
|
openUrl(authUrl);
|
|
763
|
-
|
|
838
|
+
if (!isQuick) {
|
|
839
|
+
await askReady('new.firebase.create.beforeContinue.ready.noAuth');
|
|
840
|
+
}
|
|
764
841
|
}
|
|
765
842
|
|
|
766
843
|
} else {
|
|
767
844
|
ps1.fail(tr('new.firebase.create.failed'));
|
|
768
845
|
let lastResult = setupResult;
|
|
769
846
|
while (lastResult.billingFailed && lastResult.projectId) {
|
|
770
|
-
|
|
847
|
+
const errLine = lastResult.billingQuotaError
|
|
848
|
+
? tr('new.firebase.create.billingQuotaError')
|
|
849
|
+
: `${tr('new.firebase.create.failed')}: ${lastResult.error}`;
|
|
850
|
+
ui.log.error(errLine);
|
|
771
851
|
ui.note(
|
|
772
852
|
`${kleur.cyan(lastResult.billingManualLink)}\n\n${tr('new.firebase.create.billingRetry.hint')}`,
|
|
773
853
|
tr('new.firebase.create.billingRetry.title')
|
|
@@ -784,7 +864,7 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
784
864
|
}
|
|
785
865
|
ui.log.message(tr('new.firebase.create.billingRetry.retrying'));
|
|
786
866
|
selectedBillingId = await promptBillingAccountIfNeeded(tr, cancel);
|
|
787
|
-
const ps2 = ui.makeTimedStepper();
|
|
867
|
+
const ps2 = isQuick ? ui.makeQuickStepper({ color: paintLime }) : ui.makeTimedStepper();
|
|
788
868
|
ps2.next(tr('new.firebase.create.creating'));
|
|
789
869
|
lastResult = await setupFromScratch(core.appName.trim(), core.bundleId.trim(), {
|
|
790
870
|
includeWeb: firebaseIncludeWeb,
|
|
@@ -809,19 +889,14 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
809
889
|
ui.log.info(`Project ID: ${core.firebaseProjectId}`);
|
|
810
890
|
printCreateFromScratchStatus(lastResult, tr);
|
|
811
891
|
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);
|
|
892
|
+
if (!lastResult.authEnabled) {
|
|
893
|
+
showBeforeContinue('new.firebase.create.beforeContinue.step1.noAuth', authUrl);
|
|
822
894
|
openUrl(authUrl);
|
|
823
|
-
|
|
895
|
+
if (!isQuick) {
|
|
896
|
+
await askReady('new.firebase.create.beforeContinue.ready.noAuth');
|
|
897
|
+
}
|
|
824
898
|
}
|
|
899
|
+
// Google Sign-In status is reported after flutterfire (retry path).
|
|
825
900
|
|
|
826
901
|
break;
|
|
827
902
|
} else {
|
|
@@ -874,10 +949,11 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
874
949
|
onCancel: cancel,
|
|
875
950
|
});
|
|
876
951
|
} else {
|
|
952
|
+
// Warn before the org/billing prompts so the user is ready for the long call.
|
|
953
|
+
ui.log.info(tr('new.firebase.create.estimatedTime'));
|
|
877
954
|
const selectedOrgId = await promptOrganizationIfNeeded(tr, cancel);
|
|
878
955
|
const selectedBillingId = await promptBillingAccountIfNeeded(tr, cancel);
|
|
879
|
-
ui.
|
|
880
|
-
const ps3 = ui.makeTimedStepper();
|
|
956
|
+
const ps3 = isQuick ? ui.makeQuickStepper({ color: paintLime }) : ui.makeTimedStepper();
|
|
881
957
|
ps3.next(tr('new.firebase.create.creatingPush'));
|
|
882
958
|
const setupResult = await setupFromScratch(core.appName.trim(), core.bundleId.trim(), {
|
|
883
959
|
includeWeb: true,
|
|
@@ -926,15 +1002,19 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
926
1002
|
let supabaseExistingResult = null;
|
|
927
1003
|
|
|
928
1004
|
if (backend === 'supabase') {
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
1005
|
+
if (isQuick) {
|
|
1006
|
+
supabaseCreate = true;
|
|
1007
|
+
} else {
|
|
1008
|
+
supabaseCreate = await ui.select({
|
|
1009
|
+
message: tr('new.supabase.q.create'),
|
|
1010
|
+
initialValue: true,
|
|
1011
|
+
options: [
|
|
1012
|
+
{ value: true, label: tr('new.supabase.q.create.create') },
|
|
1013
|
+
{ value: false, label: tr('new.supabase.q.create.existing') },
|
|
1014
|
+
],
|
|
1015
|
+
onCancel: cancel,
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
938
1018
|
|
|
939
1019
|
const showLoginRequired = () => {
|
|
940
1020
|
ui.log.warn(`${tr('new.supabase.loginRequired')}\n${kleur.cyan(tr('new.supabase.loginCommand'))}`);
|
|
@@ -971,12 +1051,19 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
971
1051
|
});
|
|
972
1052
|
supabaseRegion = region || DEFAULT_SUPABASE_REGION;
|
|
973
1053
|
}
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
1054
|
+
if (isQuick) {
|
|
1055
|
+
// Quick mode: generate a strong password and save to .kasy/supabase.json after build.
|
|
1056
|
+
// The user can read it from there or rotate later via Supabase dashboard.
|
|
1057
|
+
supabaseDbPassword = crypto.randomBytes(18).toString('base64')
|
|
1058
|
+
.replace(/[+/=]/g, '')
|
|
1059
|
+
.slice(0, 24);
|
|
1060
|
+
} else {
|
|
1061
|
+
supabaseDbPassword = await ui.password({
|
|
1062
|
+
message: tr('new.supabase.q.dbPassword'),
|
|
1063
|
+
validate: (v) => (v && v.length >= 6 ? undefined : tr('new.supabase.q.dbPassword.required')),
|
|
1064
|
+
onCancel: cancel,
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
980
1067
|
ui.log.info(tr('new.internet.warning'));
|
|
981
1068
|
const createSpinner = ui.timedSpinner();
|
|
982
1069
|
createSpinner.start(tr('new.supabase.creating'));
|
|
@@ -1124,20 +1211,10 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1124
1211
|
// --with flag was passed: use those modules directly, skip preset prompt.
|
|
1125
1212
|
modules = preselectedModules;
|
|
1126
1213
|
} 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] || [];
|
|
1214
|
+
// Quick mode: ship all features by default. Facebook is excluded because
|
|
1215
|
+
// it requires App ID + token that we can't auto-generate — the user adds
|
|
1216
|
+
// it later with `kasy add facebook` when they have the credentials.
|
|
1217
|
+
modules = (MODULE_PRESETS.full || []).filter((m) => m !== 'facebook');
|
|
1141
1218
|
} else {
|
|
1142
1219
|
// Advanced mode: full multiselect — built from catalog, filtered by audience + backend.
|
|
1143
1220
|
const visibleFeatures = getVisibleFeatures({ audience: KASY_AUDIENCE, backend });
|
|
@@ -1192,53 +1269,62 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1192
1269
|
const moduleAnswers = {};
|
|
1193
1270
|
|
|
1194
1271
|
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
|
-
|
|
1272
|
+
if (isQuick) {
|
|
1273
|
+
// Quick mode: ship RevenueCat scaffolded with empty keys + default paywall.
|
|
1274
|
+
// The user fills the keys later via `kasy configure revenuecat`.
|
|
1275
|
+
moduleAnswers.rcTestKey = '';
|
|
1276
|
+
moduleAnswers.rcIosProdKey = '';
|
|
1277
|
+
moduleAnswers.rcAndroidProdKey = '';
|
|
1278
|
+
moduleAnswers.defaultPaywall = 'basic';
|
|
1279
|
+
} else {
|
|
1280
|
+
// Three keys, all optional, but we require at least one. The kasy run
|
|
1281
|
+
// command picks the right key based on the device:
|
|
1282
|
+
// simulator/emulator → RC_TEST_KEY
|
|
1283
|
+
// physical iOS → RC_IOS_PROD_KEY (falls back to test if missing)
|
|
1284
|
+
// physical Android → RC_ANDROID_PROD_KEY (falls back to test if missing)
|
|
1285
|
+
moduleAnswers.rcTestKey = ((await ui.text({
|
|
1286
|
+
message: tr('new.firebase.q.revenuecat.test'),
|
|
1287
|
+
validate: (v) => {
|
|
1288
|
+
const s = (v || '').trim();
|
|
1289
|
+
if (!s) return undefined;
|
|
1290
|
+
return /^test_/.test(s) ? undefined : tr('new.firebase.q.revenuecat.test.invalid');
|
|
1291
|
+
},
|
|
1292
|
+
onCancel: cancel,
|
|
1293
|
+
})) || '').trim();
|
|
1294
|
+
moduleAnswers.rcIosProdKey = ((await ui.text({
|
|
1295
|
+
message: tr('new.firebase.q.revenuecat.iosProd'),
|
|
1296
|
+
validate: (v) => {
|
|
1297
|
+
const s = (v || '').trim();
|
|
1298
|
+
if (!s) return undefined;
|
|
1299
|
+
return /^appl_/.test(s) ? undefined : tr('new.firebase.q.revenuecat.iosProd.invalid');
|
|
1300
|
+
},
|
|
1301
|
+
onCancel: cancel,
|
|
1302
|
+
})) || '').trim();
|
|
1303
|
+
moduleAnswers.rcAndroidProdKey = ((await ui.text({
|
|
1304
|
+
message: tr('new.firebase.q.revenuecat.androidProd'),
|
|
1305
|
+
validate: (v) => {
|
|
1306
|
+
const s = (v || '').trim();
|
|
1307
|
+
if (!s) return undefined;
|
|
1308
|
+
return /^goog_/.test(s) ? undefined : tr('new.firebase.q.revenuecat.androidProd.invalid');
|
|
1309
|
+
},
|
|
1310
|
+
onCancel: cancel,
|
|
1311
|
+
})) || '').trim();
|
|
1312
|
+
if (!moduleAnswers.rcTestKey && !moduleAnswers.rcIosProdKey && !moduleAnswers.rcAndroidProdKey) {
|
|
1313
|
+
// Non-blocking: user can fill .env later. Just warn so they're not surprised.
|
|
1314
|
+
ui.log.warn(tr('new.firebase.q.revenuecat.atLeastOne'));
|
|
1315
|
+
}
|
|
1316
|
+
moduleAnswers.defaultPaywall = await ui.select({
|
|
1317
|
+
message: tr('new.firebase.q.paywall'),
|
|
1318
|
+
initialValue: 'basic',
|
|
1319
|
+
options: [
|
|
1320
|
+
{ value: 'basic', label: 'Basic (list of plans)' },
|
|
1321
|
+
{ value: 'withSwitch', label: 'With trial switch' },
|
|
1322
|
+
{ value: 'basicRow', label: 'Row + comparison table' },
|
|
1323
|
+
{ value: 'minimal', label: 'Minimal (benefits + CTA)' },
|
|
1324
|
+
],
|
|
1325
|
+
onCancel: cancel,
|
|
1326
|
+
});
|
|
1230
1327
|
}
|
|
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
1328
|
}
|
|
1243
1329
|
|
|
1244
1330
|
// RC web key — only in advanced mode (optional credential, can configure later).
|
|
@@ -1314,8 +1400,9 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1314
1400
|
moduleAnswers.llmConfigureLater = true;
|
|
1315
1401
|
}
|
|
1316
1402
|
|
|
1317
|
-
// Facebook — required credentials
|
|
1318
|
-
|
|
1403
|
+
// Facebook — required credentials. Quick mode ships scaffolded but empty
|
|
1404
|
+
// (user adds credentials later via `kasy configure facebook`).
|
|
1405
|
+
if (modules.includes('facebook') && !isQuick) {
|
|
1319
1406
|
moduleAnswers.fbAppId = await ui.text({
|
|
1320
1407
|
message: tr('new.firebase.q.facebook.appId'),
|
|
1321
1408
|
validate: (v) => {
|
|
@@ -1329,6 +1416,9 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1329
1416
|
validate: (v) => (v && v.trim() ? undefined : tr('new.firebase.q.facebook.token.required')),
|
|
1330
1417
|
onCancel: cancel,
|
|
1331
1418
|
});
|
|
1419
|
+
} else if (modules.includes('facebook') && isQuick) {
|
|
1420
|
+
moduleAnswers.fbAppId = '';
|
|
1421
|
+
moduleAnswers.fbToken = '';
|
|
1332
1422
|
}
|
|
1333
1423
|
|
|
1334
1424
|
// Server secrets (webhook, Meta Ads) — skip in quick mode, configure later via `kasy deploy`.
|
|
@@ -1384,9 +1474,10 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1384
1474
|
apiBaseUrl: core.apiBaseUrl?.trim(),
|
|
1385
1475
|
};
|
|
1386
1476
|
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
if (!yes) {
|
|
1477
|
+
// Step-by-step mode shows summary before generating so the user can confirm.
|
|
1478
|
+
// Quick mode skips it here — the summary is shown at the end with the success card.
|
|
1479
|
+
if (!yes && !isQuick) {
|
|
1480
|
+
printSummary(tr, answers);
|
|
1390
1481
|
const proceed = await ui.confirm({
|
|
1391
1482
|
message: tr('new.firebase.confirm.proceed'),
|
|
1392
1483
|
initialValue: true,
|
|
@@ -1399,10 +1490,8 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1399
1490
|
}
|
|
1400
1491
|
|
|
1401
1492
|
// ── Generate ────────────────────────────────────────────────────────────
|
|
1402
|
-
//
|
|
1403
|
-
|
|
1404
|
-
// spinner with a mutating message.
|
|
1405
|
-
const stepper = ui.makeTimedStepper();
|
|
1493
|
+
// Quick: single rolling line that mutates message. Advanced: stack each step.
|
|
1494
|
+
const stepper = isQuick ? ui.makeQuickStepper({ color: paintLime }) : ui.makeTimedStepper();
|
|
1406
1495
|
// First step started here so even silent prep work shows progress.
|
|
1407
1496
|
stepper.next(stepProgress('project-setup', language));
|
|
1408
1497
|
|
|
@@ -1448,6 +1537,10 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1448
1537
|
functionsRegion: firebaseRegion,
|
|
1449
1538
|
language,
|
|
1450
1539
|
onProgress,
|
|
1540
|
+
// Google Sign-In is enabled in a later step via Firebase CLI, which is
|
|
1541
|
+
// when the OAuth Web Client + REVERSED_CLIENT_ID get created. Defer the
|
|
1542
|
+
// two patches that depend on those IDs so they don't fail noisily here.
|
|
1543
|
+
deferGoogleAuthPatches: true,
|
|
1451
1544
|
});
|
|
1452
1545
|
}
|
|
1453
1546
|
// Close the last in-flight step. We don't need a label — its own message
|
|
@@ -1507,7 +1600,13 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1507
1600
|
// only way to make the client secret available via the Identity Toolkit API.
|
|
1508
1601
|
// Non-fatal: silently ignored if it fails (e.g. API not yet propagated).
|
|
1509
1602
|
if (answers.firebaseProjectId) {
|
|
1510
|
-
await enableAuthProviders(answers.firebaseProjectId);
|
|
1603
|
+
const authResult = await enableAuthProviders(answers.firebaseProjectId);
|
|
1604
|
+
if (authResult.ok && !authResult.googleSignInSkipped) {
|
|
1605
|
+
printStepResult({ name: 'google sign-in', ok: true }, language);
|
|
1606
|
+
}
|
|
1607
|
+
if (authResult.appleEnabled) {
|
|
1608
|
+
printStepResult({ name: 'apple sign-in', ok: true }, language);
|
|
1609
|
+
}
|
|
1511
1610
|
}
|
|
1512
1611
|
|
|
1513
1612
|
// Try to get the Client Secret from Identity Toolkit API (requires gcloud auth)
|
|
@@ -1571,6 +1670,31 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1571
1670
|
setupSpinner.error(err.message);
|
|
1572
1671
|
ui.log.warn(tr('new.supabase.setupManual'));
|
|
1573
1672
|
}
|
|
1673
|
+
|
|
1674
|
+
// Quick mode generates the DB password automatically — persist it so the
|
|
1675
|
+
// user can recover it later (e.g. to log into Supabase Studio).
|
|
1676
|
+
if (isQuick && supabaseCreate && supabaseDbPassword) {
|
|
1677
|
+
try {
|
|
1678
|
+
const kasyDir = path.join(targetDir, '.kasy');
|
|
1679
|
+
await fs.ensureDir(kasyDir);
|
|
1680
|
+
await fs.writeJson(
|
|
1681
|
+
path.join(kasyDir, 'supabase.json'),
|
|
1682
|
+
{ projectRef: supabaseSetupPayload.projectRef, dbPassword: supabaseDbPassword },
|
|
1683
|
+
{ spaces: 2 }
|
|
1684
|
+
);
|
|
1685
|
+
const gitignorePath = path.join(targetDir, '.gitignore');
|
|
1686
|
+
const gitignoreEntry = '.kasy/supabase.json';
|
|
1687
|
+
if (await fs.pathExists(gitignorePath)) {
|
|
1688
|
+
const existing = await fs.readFile(gitignorePath, 'utf8');
|
|
1689
|
+
if (!existing.includes(gitignoreEntry)) {
|
|
1690
|
+
await fs.appendFile(gitignorePath, `\n# Supabase auto-generated DB password (credentials — do not commit)\n${gitignoreEntry}\n`, 'utf8');
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
ui.log.info(tr('new.supabase.passwordSaved'));
|
|
1694
|
+
} catch (_) {
|
|
1695
|
+
// Non-fatal: user can rotate password via Supabase dashboard if needed.
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1574
1698
|
}
|
|
1575
1699
|
}
|
|
1576
1700
|
|
|
@@ -1640,16 +1764,57 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1640
1764
|
);
|
|
1641
1765
|
}
|
|
1642
1766
|
|
|
1643
|
-
// ──
|
|
1644
|
-
//
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1767
|
+
// ── Google + Apple Sign-In: use Firebase CLI for Google (creates OAuth client),
|
|
1768
|
+
// then REST API for Apple as best-effort. ───────────────────────────────────
|
|
1769
|
+
// Firebase CLI's `deploy --only auth` is the only documented path that auto-
|
|
1770
|
+
// creates the OAuth 2.0 Web Client without manual Console clicks — the same
|
|
1771
|
+
// backend that the Console hits internally.
|
|
1772
|
+
// (Supabase backend keeps the REST-only retry — its OAuth client is provisioned
|
|
1773
|
+
// differently via the linked Firebase Web app.)
|
|
1774
|
+
if (backend === 'firebase' && answers.firebaseProjectId && flutterfireStep?.ok) {
|
|
1775
|
+
const googleSpinner = ui.spinner();
|
|
1776
|
+
googleSpinner.start(tr('new.google.enabling'));
|
|
1777
|
+
const cliResult = await enableAuthViaFirebaseCli({
|
|
1778
|
+
projectDir: targetDir,
|
|
1779
|
+
projectId: answers.firebaseProjectId,
|
|
1780
|
+
appName: answers.appName,
|
|
1781
|
+
});
|
|
1782
|
+
googleSpinner.stop(tr('new.google.enabling'));
|
|
1783
|
+
if (cliResult.ok) {
|
|
1784
|
+
printStepResult({ name: 'google sign-in', ok: true }, language);
|
|
1785
|
+
|
|
1786
|
+
// Google deploy created the OAuth Web Client + iOS client. Re-run flutterfire
|
|
1787
|
+
// so google-services.json and GoogleService-Info.plist pick up the new IDs,
|
|
1788
|
+
// then re-apply the two patches that failed during the initial pass.
|
|
1789
|
+
const rerunSpinner = ui.spinner();
|
|
1790
|
+
rerunSpinner.start(tr('new.google.refreshConfigs'));
|
|
1791
|
+
const ffRerun = await flutterfireConfigure(targetDir, answers.firebaseProjectId, {
|
|
1792
|
+
includeWeb: answers.includeWeb !== false,
|
|
1793
|
+
});
|
|
1794
|
+
rerunSpinner.stop(tr('new.google.refreshConfigs'));
|
|
1795
|
+
if (ffRerun.ok) {
|
|
1796
|
+
const gaResult = await writeGoogleAuthOptions(targetDir);
|
|
1797
|
+
printStepResult({ name: 'google-auth-options', ok: gaResult.ok, detail: gaResult.error }, language);
|
|
1798
|
+
const iosResult = await writeGoogleIosUrlScheme(targetDir);
|
|
1799
|
+
printStepResult({ name: 'google-ios-url-scheme', ok: iosResult.ok, detail: iosResult.error }, language);
|
|
1800
|
+
}
|
|
1801
|
+
} else {
|
|
1802
|
+
const authUrl = `https://console.firebase.google.com/project/${answers.firebaseProjectId}/authentication/providers`;
|
|
1803
|
+
const reason = cliResult.error === 'support_email_required'
|
|
1804
|
+
? tr('new.google.manualHint.noEmail')
|
|
1805
|
+
: tr('new.google.manualHint');
|
|
1806
|
+
ui.log.warn(`${reason}\n${kleur.cyan(authUrl)}`);
|
|
1807
|
+
}
|
|
1808
|
+
// Apple Sign-In remains a best-effort REST POST (no CLI support yet).
|
|
1809
|
+
const appleResult = await enableAuthProviders(answers.firebaseProjectId);
|
|
1810
|
+
if (appleResult.appleEnabled) {
|
|
1811
|
+
printStepResult({ name: 'apple sign-in', ok: true }, language);
|
|
1812
|
+
}
|
|
1651
1813
|
}
|
|
1652
1814
|
|
|
1815
|
+
// APNs key (iOS push) is intentionally not mentioned here — it only becomes
|
|
1816
|
+
// relevant when shipping to iOS, and the docs at kasy.dev/docs/apns explain it.
|
|
1817
|
+
|
|
1653
1818
|
printSuccessCard(tr, answers, targetDir);
|
|
1654
1819
|
|
|
1655
1820
|
ui.outro(kleur.bold(tr('new.success.title')));
|