kasy-cli 1.18.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 +1 -0
- package/lib/commands/new.js +99 -105
- package/lib/commands/run.js +16 -3
- package/lib/scaffold/backends/firebase/setup-from-scratch.js +79 -0
- package/lib/utils/brand.js +1 -1
- package/lib/utils/i18n/messages-en.js +4 -0
- package/lib/utils/i18n/messages-es.js +4 -0
- package/lib/utils/i18n/messages-pt.js +4 -0
- package/package.json +1 -2
- package/templates/firebase/lib/components/kasy_date_picker.dart +1670 -331
- package/templates/firebase/lib/components/kasy_tabs.dart +111 -72
- 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/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 +457 -73
- 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/main.dart +34 -34
- package/templates/firebase/pubspec.yaml +1 -0
- package/templates/firebase/storage.cors.json +8 -0
- package/templates/firebase/web/index.html +15 -2
- package/templates/firebase/lib/core/bottom_menu/kasy_bart_navigation.dart +0 -22
package/bin/kasy.js
CHANGED
|
@@ -355,6 +355,7 @@ function buildProgram(language) {
|
|
|
355
355
|
.option('--ios', 'Run on iOS simulator/device')
|
|
356
356
|
.option('--android', 'Run on Android emulator/device')
|
|
357
357
|
.option('--web', 'Run on Chrome (web)')
|
|
358
|
+
.option('--web-port <port>', 'Fixed port for Chrome (default 5555) — keeps the origin stable so Firebase Auth sessions persist between runs')
|
|
358
359
|
.option('-d, --device <id>', 'Run on specific device ID')
|
|
359
360
|
.option('--prod', 'Use production dart-defines (from launch.json)')
|
|
360
361
|
.option('--no-defines', 'Skip dart-defines from launch.json')
|
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');
|
|
@@ -187,26 +187,6 @@ function printPrerequisites(tr, backend, firebaseSetupMode = 'existing', checkRe
|
|
|
187
187
|
}
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
-
function printSummary(tr, answers) {
|
|
191
|
-
const modules = answers.modules.length > 0
|
|
192
|
-
? answers.modules.join(', ')
|
|
193
|
-
: tr('new.firebase.confirm.none');
|
|
194
|
-
|
|
195
|
-
const backendLabel = { firebase: '🔥 Firebase', supabase: '🟢 Supabase', api: '🔗 API REST' }[answers.backend] || answers.backend;
|
|
196
|
-
|
|
197
|
-
const rows = [
|
|
198
|
-
`${kleur.dim(tr('new.firebase.confirm.app') + ':')} ${kleur.white(answers.appName)}`,
|
|
199
|
-
`${kleur.dim('Bundle:')} ${kleur.white(answers.bundleId)}`,
|
|
200
|
-
`${kleur.dim('Backend:')} ${kleur.white(backendLabel)}`,
|
|
201
|
-
];
|
|
202
|
-
if (answers.firebaseProjectId) rows.push(`${kleur.dim('Firebase:')} ${kleur.white(answers.firebaseProjectId)}`);
|
|
203
|
-
if (answers.supabaseUrl) rows.push(`${kleur.dim('Supabase URL:')} ${kleur.white(answers.supabaseUrl)}`);
|
|
204
|
-
if (answers.apiBaseUrl) rows.push(`${kleur.dim('API URL:')} ${kleur.white(answers.apiBaseUrl)}`);
|
|
205
|
-
rows.push(`${kleur.dim(tr('new.firebase.confirm.modules') + ':')} ${kleur.white(modules)}`);
|
|
206
|
-
|
|
207
|
-
console.log(infoBox(`📦 ${tr('new.firebase.confirm.title')}`, rows.join('\n')));
|
|
208
|
-
}
|
|
209
|
-
|
|
210
190
|
const STEP_LABELS = {
|
|
211
191
|
'project-setup': { en: 'Project configured', pt: 'Projeto configurado', es: 'Proyecto configurado' },
|
|
212
192
|
'pub-get': { en: 'Packages installed', pt: 'Pacotes instalados', es: 'Paquetes instalados' },
|
|
@@ -254,6 +234,7 @@ const STEP_LABELS = {
|
|
|
254
234
|
'service-account': { en: 'Service account key created', pt: 'Chave de conta de servico criada', es: 'Clave de cuenta de servicio creada' },
|
|
255
235
|
'firestore': { en: 'Firestore database created', pt: 'Banco Firestore criado', es: 'Base de datos Firestore creada' },
|
|
256
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)' },
|
|
257
238
|
};
|
|
258
239
|
|
|
259
240
|
const STEP_PROGRESS = {
|
|
@@ -275,6 +256,7 @@ const STEP_PROGRESS = {
|
|
|
275
256
|
'service-account': { en: 'Creating service account key…', pt: 'Criando chave de conta de servico…', es: 'Creando clave de cuenta de servicio…' },
|
|
276
257
|
'firestore': { en: 'Creating Firestore database…', pt: 'Criando banco Firestore…', es: 'Creando base de datos Firestore…' },
|
|
277
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…' },
|
|
278
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)' },
|
|
279
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!)' },
|
|
280
262
|
};
|
|
@@ -632,7 +614,13 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
632
614
|
await ensureBilling();
|
|
633
615
|
}
|
|
634
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
|
+
|
|
635
622
|
// ── Bundle ID — Quick uses derived value silently; Step-by-step lets user override ─
|
|
623
|
+
section('new.advanced.section.config');
|
|
636
624
|
if (!isQuick) {
|
|
637
625
|
const bundleId = await ui.text({
|
|
638
626
|
message: tr('new.firebase.q.bundleId'),
|
|
@@ -767,7 +755,7 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
767
755
|
ui.log.info(tr('new.firebase.create.estimatedTime'));
|
|
768
756
|
const selectedOrgId = await promptOrganizationIfNeeded(tr, cancel);
|
|
769
757
|
let selectedBillingId = await promptBillingAccountIfNeeded(tr, cancel);
|
|
770
|
-
const ps1 =
|
|
758
|
+
const ps1 = ui.makeQuickStepper({ color: paintLime });
|
|
771
759
|
ps1.next(tr('new.firebase.create.creating'));
|
|
772
760
|
const ps1OnProgress = (key, data) => {
|
|
773
761
|
if (key === 'wait-propagate') {
|
|
@@ -776,6 +764,11 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
776
764
|
ps1.next(stepProgress('firestore', language));
|
|
777
765
|
} else if (key === 'storage') {
|
|
778
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 || '')}`);
|
|
779
772
|
} else if (key === 'auth-providers-warn') {
|
|
780
773
|
ps1.stop();
|
|
781
774
|
ui.log.warn(`${tr('new.firebase.interactive.authWarn')}\n${kleur.cyan(data?.url || '')}`);
|
|
@@ -864,7 +857,7 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
864
857
|
}
|
|
865
858
|
ui.log.message(tr('new.firebase.create.billingRetry.retrying'));
|
|
866
859
|
selectedBillingId = await promptBillingAccountIfNeeded(tr, cancel);
|
|
867
|
-
const ps2 =
|
|
860
|
+
const ps2 = ui.makeQuickStepper({ color: paintLime });
|
|
868
861
|
ps2.next(tr('new.firebase.create.creating'));
|
|
869
862
|
lastResult = await setupFromScratch(core.appName.trim(), core.bundleId.trim(), {
|
|
870
863
|
includeWeb: firebaseIncludeWeb,
|
|
@@ -953,7 +946,7 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
953
946
|
ui.log.info(tr('new.firebase.create.estimatedTime'));
|
|
954
947
|
const selectedOrgId = await promptOrganizationIfNeeded(tr, cancel);
|
|
955
948
|
const selectedBillingId = await promptBillingAccountIfNeeded(tr, cancel);
|
|
956
|
-
const ps3 =
|
|
949
|
+
const ps3 = ui.makeQuickStepper({ color: paintLime });
|
|
957
950
|
ps3.next(tr('new.firebase.create.creatingPush'));
|
|
958
951
|
const setupResult = await setupFromScratch(core.appName.trim(), core.bundleId.trim(), {
|
|
959
952
|
includeWeb: true,
|
|
@@ -1162,7 +1155,7 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1162
1155
|
|
|
1163
1156
|
// ── Firebase existing project: enable APIs + create Firestore/Storage ───
|
|
1164
1157
|
if (backend === 'firebase' && firebaseSetupMode === 'existing' && core.firebaseProjectId) {
|
|
1165
|
-
const ps4 = ui.
|
|
1158
|
+
const ps4 = ui.makeQuickStepper({ color: paintLime });
|
|
1166
1159
|
ps4.next(stepProgress('enable-apis', language));
|
|
1167
1160
|
const existingSetup = await setupExistingProject(core.firebaseProjectId, {
|
|
1168
1161
|
onProgress: (key, data) => {
|
|
@@ -1175,6 +1168,11 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1175
1168
|
ps4.next(stepProgress('firestore', language));
|
|
1176
1169
|
} else if (key === 'storage') {
|
|
1177
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 || '')}`);
|
|
1178
1176
|
} else if (key === 'auth-providers-warn') {
|
|
1179
1177
|
ps4.stop();
|
|
1180
1178
|
ui.log.warn(`${tr('new.firebase.interactive.authWarn')}\n${kleur.cyan(data?.url || '')}`);
|
|
@@ -1216,6 +1214,7 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1216
1214
|
// it later with `kasy add facebook` when they have the credentials.
|
|
1217
1215
|
modules = (MODULE_PRESETS.full || []).filter((m) => m !== 'facebook');
|
|
1218
1216
|
} else {
|
|
1217
|
+
section('new.advanced.section.features');
|
|
1219
1218
|
// Advanced mode: full multiselect — built from catalog, filtered by audience + backend.
|
|
1220
1219
|
const visibleFeatures = getVisibleFeatures({ audience: KASY_AUDIENCE, backend });
|
|
1221
1220
|
|
|
@@ -1251,10 +1250,15 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1251
1250
|
}
|
|
1252
1251
|
}
|
|
1253
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');
|
|
1254
1258
|
const rawModules = await ui.multiselect({
|
|
1255
1259
|
message: tr('new.firebase.q.modules'),
|
|
1256
1260
|
options: moduleOptions,
|
|
1257
|
-
initialValues:
|
|
1261
|
+
initialValues: defaultSelected,
|
|
1258
1262
|
required: false,
|
|
1259
1263
|
onCancel: cancel,
|
|
1260
1264
|
});
|
|
@@ -1265,13 +1269,30 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1265
1269
|
modules = [...new Set([...modules, 'web'])];
|
|
1266
1270
|
}
|
|
1267
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
|
+
|
|
1268
1289
|
// ── Module-specific questions ───────────────────────────────────────────
|
|
1269
1290
|
const moduleAnswers = {};
|
|
1270
1291
|
|
|
1271
1292
|
if (modules.includes('revenuecat')) {
|
|
1272
|
-
if (
|
|
1273
|
-
//
|
|
1274
|
-
//
|
|
1293
|
+
if (!askCreds) {
|
|
1294
|
+
// Defer mode: scaffold with empty keys + default paywall; user fills
|
|
1295
|
+
// them later via `kasy configure revenuecat`.
|
|
1275
1296
|
moduleAnswers.rcTestKey = '';
|
|
1276
1297
|
moduleAnswers.rcIosProdKey = '';
|
|
1277
1298
|
moduleAnswers.rcAndroidProdKey = '';
|
|
@@ -1327,26 +1348,26 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1327
1348
|
}
|
|
1328
1349
|
}
|
|
1329
1350
|
|
|
1330
|
-
// RC web key — only
|
|
1331
|
-
if (
|
|
1332
|
-
const rcWebKey = await ui.text({
|
|
1333
|
-
message: tr('new.firebase.q.revenuecat.webKey'),
|
|
1334
|
-
validate: (v) => {
|
|
1335
|
-
if (!v || !v.trim()) return undefined; // optional — blank is fine
|
|
1336
|
-
return /^rcb_/.test(v.trim()) ? undefined : tr('new.firebase.q.revenuecat.webKey.invalid');
|
|
1337
|
-
},
|
|
1338
|
-
onCancel: cancel,
|
|
1339
|
-
});
|
|
1351
|
+
// RC web key — only when configuring inline. Otherwise mark scaffolded.
|
|
1352
|
+
if (modules.includes('revenuecat') && modules.includes('web')) {
|
|
1340
1353
|
moduleAnswers.revenuecatWeb = true;
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
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
|
+
}
|
|
1346
1367
|
}
|
|
1347
1368
|
|
|
1348
|
-
// Sentry DSN —
|
|
1349
|
-
if (
|
|
1369
|
+
// Sentry DSN — only when configuring inline.
|
|
1370
|
+
if (askCreds && modules.includes('sentry')) {
|
|
1350
1371
|
moduleAnswers.sentryDsn = await ui.text({
|
|
1351
1372
|
message: tr('new.firebase.q.sentry.dsn'),
|
|
1352
1373
|
validate: (v) => {
|
|
@@ -1359,23 +1380,17 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1359
1380
|
});
|
|
1360
1381
|
}
|
|
1361
1382
|
|
|
1362
|
-
// Mixpanel token —
|
|
1363
|
-
if (
|
|
1383
|
+
// Mixpanel token — only when configuring inline.
|
|
1384
|
+
if (askCreds && modules.includes('analytics')) {
|
|
1364
1385
|
moduleAnswers.mixpanelToken = await ui.text({
|
|
1365
1386
|
message: tr('new.firebase.q.mixpanel.token'),
|
|
1366
1387
|
onCancel: cancel,
|
|
1367
1388
|
});
|
|
1368
1389
|
}
|
|
1369
1390
|
|
|
1370
|
-
// LLM Chat credentials
|
|
1371
|
-
if (
|
|
1372
|
-
|
|
1373
|
-
message: tr('new.q.llm_chat.configureNow'),
|
|
1374
|
-
initialValue: true,
|
|
1375
|
-
onCancel: cancel,
|
|
1376
|
-
});
|
|
1377
|
-
|
|
1378
|
-
if (configureLlmNow) {
|
|
1391
|
+
// LLM Chat credentials.
|
|
1392
|
+
if (modules.includes('llm_chat')) {
|
|
1393
|
+
if (askCreds) {
|
|
1379
1394
|
moduleAnswers.llmProvider = await ui.select({
|
|
1380
1395
|
message: tr('add.prompt.llmProvider'),
|
|
1381
1396
|
initialValue: 'openai',
|
|
@@ -1396,40 +1411,33 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1396
1411
|
} else {
|
|
1397
1412
|
moduleAnswers.llmConfigureLater = true;
|
|
1398
1413
|
}
|
|
1399
|
-
} else if (isQuick && modules.includes('llm_chat')) {
|
|
1400
|
-
moduleAnswers.llmConfigureLater = true;
|
|
1401
1414
|
}
|
|
1402
1415
|
|
|
1403
|
-
// Facebook — required credentials
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1416
|
+
// Facebook — required credentials when inline; scaffold-only otherwise.
|
|
1417
|
+
if (modules.includes('facebook')) {
|
|
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
|
+
}
|
|
1422
1436
|
}
|
|
1423
1437
|
|
|
1424
|
-
// Server secrets (webhook, Meta Ads) —
|
|
1425
|
-
if (
|
|
1426
|
-
|
|
1427
|
-
message: tr('new.firebase.q.secrets.configureNow'),
|
|
1428
|
-
initialValue: true,
|
|
1429
|
-
onCancel: cancel,
|
|
1430
|
-
});
|
|
1431
|
-
|
|
1432
|
-
if (configureSecretsNow) {
|
|
1438
|
+
// Server secrets (webhook, Meta Ads) — only when configuring inline.
|
|
1439
|
+
if (modules.includes('revenuecat') && (backend === 'supabase' || backend === 'firebase')) {
|
|
1440
|
+
if (askCreds) {
|
|
1433
1441
|
moduleAnswers.rcWebhookKey = await ui.text({
|
|
1434
1442
|
message: tr('new.firebase.q.revenuecat.webhookKey'),
|
|
1435
1443
|
initialValue: generateWebhookKey(),
|
|
@@ -1451,9 +1459,6 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1451
1459
|
} else {
|
|
1452
1460
|
moduleAnswers.secretsConfigureLater = true;
|
|
1453
1461
|
}
|
|
1454
|
-
} else if (isQuick && modules.includes('revenuecat')) {
|
|
1455
|
-
// Quick mode: always defer secrets to `kasy deploy`.
|
|
1456
|
-
moduleAnswers.secretsConfigureLater = true;
|
|
1457
1462
|
}
|
|
1458
1463
|
|
|
1459
1464
|
// ── Deploy is now a separate command (`kasy deploy`) ─────────────
|
|
@@ -1474,24 +1479,13 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1474
1479
|
apiBaseUrl: core.apiBaseUrl?.trim(),
|
|
1475
1480
|
};
|
|
1476
1481
|
|
|
1477
|
-
//
|
|
1478
|
-
//
|
|
1479
|
-
|
|
1480
|
-
printSummary(tr, answers);
|
|
1481
|
-
const proceed = await ui.confirm({
|
|
1482
|
-
message: tr('new.firebase.confirm.proceed'),
|
|
1483
|
-
initialValue: true,
|
|
1484
|
-
onCancel: cancel,
|
|
1485
|
-
});
|
|
1486
|
-
if (!proceed) {
|
|
1487
|
-
ui.cancel(tr('prompt.cancelled'));
|
|
1488
|
-
process.exit(0);
|
|
1489
|
-
}
|
|
1490
|
-
}
|
|
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.
|
|
1491
1485
|
|
|
1492
1486
|
// ── Generate ────────────────────────────────────────────────────────────
|
|
1493
1487
|
// Quick: single rolling line that mutates message. Advanced: stack each step.
|
|
1494
|
-
const stepper =
|
|
1488
|
+
const stepper = ui.makeQuickStepper({ color: paintLime });
|
|
1495
1489
|
// First step started here so even silent prep work shows progress.
|
|
1496
1490
|
stepper.next(stepProgress('project-setup', language));
|
|
1497
1491
|
|
package/lib/commands/run.js
CHANGED
|
@@ -251,18 +251,22 @@ async function runRun(directory, options = {}) {
|
|
|
251
251
|
const deviceArgs = [];
|
|
252
252
|
let resolvedDeviceLabel = null;
|
|
253
253
|
let pickedDevice = null;
|
|
254
|
+
let isChromeTarget = false;
|
|
254
255
|
if (options.web) {
|
|
256
|
+
isChromeTarget = true;
|
|
255
257
|
deviceArgs.push('-d', 'chrome');
|
|
256
258
|
} else if (options.ios) {
|
|
257
259
|
deviceArgs.push('-d', 'ios');
|
|
258
260
|
} else if (options.android) {
|
|
259
261
|
deviceArgs.push('-d', 'android');
|
|
260
262
|
} else if (options.device) {
|
|
263
|
+
if (options.device === 'chrome' || options.device === 'web-server') {
|
|
264
|
+
isChromeTarget = true;
|
|
265
|
+
}
|
|
261
266
|
deviceArgs.push('-d', options.device);
|
|
262
267
|
} else {
|
|
263
268
|
const devices = listFlutterDevices(projectDir);
|
|
264
269
|
if (devices.length > 1) {
|
|
265
|
-
printCompactHeader(t);
|
|
266
270
|
pickedDevice = await pickDevice(devices, t);
|
|
267
271
|
if (!pickedDevice) {
|
|
268
272
|
console.log(kleur.yellow(` ⚠ ${t('run.warn.nothingSelected')}`));
|
|
@@ -270,9 +274,18 @@ async function runRun(directory, options = {}) {
|
|
|
270
274
|
}
|
|
271
275
|
deviceArgs.push('-d', pickedDevice.id);
|
|
272
276
|
resolvedDeviceLabel = `${pickedDevice.name} (${pickedDevice.id})`;
|
|
277
|
+
if (classifyDevice(pickedDevice) === 'web') isChromeTarget = true;
|
|
278
|
+
} else if (devices.length === 1 && classifyDevice(devices[0]) === 'web') {
|
|
279
|
+
// Single auto-picked Chrome — still force the fixed port.
|
|
280
|
+
isChromeTarget = true;
|
|
273
281
|
}
|
|
274
|
-
|
|
275
|
-
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (isChromeTarget) {
|
|
285
|
+
// Pin a fixed port so the Chrome origin stays the same between runs.
|
|
286
|
+
// Firebase Auth persists sessions per-origin (IndexedDB) — a random port
|
|
287
|
+
// each run means the user gets logged out every restart.
|
|
288
|
+
deviceArgs.push('--web-port', options.webPort || '5555');
|
|
276
289
|
}
|
|
277
290
|
|
|
278
291
|
// Read dart-defines from .vscode/launch.json (skip if --no-defines)
|
|
@@ -317,6 +317,66 @@ async function createFirebaseStorageBucket(projectId, location = 'us-central1')
|
|
|
317
317
|
return { ok: false, error: 'Max retries exceeded' };
|
|
318
318
|
}
|
|
319
319
|
|
|
320
|
+
/**
|
|
321
|
+
* Apply default CORS config to a Cloud Storage bucket so the browser can load
|
|
322
|
+
* objects (e.g. user avatars) from a Flutter web app. Without this the GET
|
|
323
|
+
* request hits a CORS error and the image silently fails to render.
|
|
324
|
+
*
|
|
325
|
+
* Tries the Firebase default bucket name first (<projectId>.firebasestorage.app
|
|
326
|
+
* for projects created after Oct 2024, <projectId>.appspot.com for legacy
|
|
327
|
+
* projects) and falls back to the other if the first 404s. Safe to call
|
|
328
|
+
* repeatedly — it overwrites the bucket-level cors config.
|
|
329
|
+
*
|
|
330
|
+
* Reads the policy from cli/templates/firebase/storage.cors.json so the same
|
|
331
|
+
* file is shipped to the user's project for visibility and manual re-runs.
|
|
332
|
+
*
|
|
333
|
+
* @param {string} projectId
|
|
334
|
+
* @returns {{ ok: boolean, bucket?: string, error?: string }}
|
|
335
|
+
*/
|
|
336
|
+
async function applyStorageCors(projectId, options = {}) {
|
|
337
|
+
const { corsFilePath } = options;
|
|
338
|
+
const defaultCorsPath = path.join(__dirname, '..', '..', '..', '..', 'templates', 'firebase', 'storage.cors.json');
|
|
339
|
+
const resolvedPath = corsFilePath || defaultCorsPath;
|
|
340
|
+
let corsConfig;
|
|
341
|
+
try {
|
|
342
|
+
corsConfig = await fs.readJson(resolvedPath);
|
|
343
|
+
} catch (e) {
|
|
344
|
+
return { ok: false, error: `Failed to read CORS config at ${resolvedPath}: ${e.message}` };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
let token;
|
|
348
|
+
try {
|
|
349
|
+
token = await getAccessToken();
|
|
350
|
+
} catch (e) {
|
|
351
|
+
return { ok: false, error: 'Could not get access token' };
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const candidates = [
|
|
355
|
+
`${projectId}.firebasestorage.app`,
|
|
356
|
+
`${projectId}.appspot.com`,
|
|
357
|
+
];
|
|
358
|
+
|
|
359
|
+
let lastError = null;
|
|
360
|
+
for (const bucket of candidates) {
|
|
361
|
+
const url = `https://storage.googleapis.com/storage/v1/b/${bucket}?fields=cors`;
|
|
362
|
+
const res = await fetch(url, {
|
|
363
|
+
method: 'PATCH',
|
|
364
|
+
headers: {
|
|
365
|
+
Authorization: `Bearer ${token}`,
|
|
366
|
+
'Content-Type': 'application/json',
|
|
367
|
+
'X-Goog-User-Project': projectId,
|
|
368
|
+
},
|
|
369
|
+
body: JSON.stringify({ cors: corsConfig }),
|
|
370
|
+
});
|
|
371
|
+
if (res.ok) return { ok: true, bucket };
|
|
372
|
+
const text = await res.text();
|
|
373
|
+
lastError = `${res.status}: ${text}`;
|
|
374
|
+
// 404 means the bucket name doesn't exist — try the next candidate.
|
|
375
|
+
if (res.status !== 404) break;
|
|
376
|
+
}
|
|
377
|
+
return { ok: false, error: lastError || 'No matching bucket found' };
|
|
378
|
+
}
|
|
379
|
+
|
|
320
380
|
/**
|
|
321
381
|
* Enable required Google Cloud APIs.
|
|
322
382
|
*/
|
|
@@ -864,6 +924,15 @@ async function setupFromScratch(appName, bundleId, options = {}) {
|
|
|
864
924
|
error: storageResult.error,
|
|
865
925
|
url: `https://console.firebase.google.com/project/${projectId}/storage`,
|
|
866
926
|
});
|
|
927
|
+
} else {
|
|
928
|
+
onProgress('storage-cors');
|
|
929
|
+
const corsResult = await applyStorageCors(projectId);
|
|
930
|
+
if (!corsResult.ok) {
|
|
931
|
+
onProgress('storage-cors-warn', {
|
|
932
|
+
error: corsResult.error,
|
|
933
|
+
url: `https://console.cloud.google.com/storage/browser?project=${projectId}`,
|
|
934
|
+
});
|
|
935
|
+
}
|
|
867
936
|
}
|
|
868
937
|
|
|
869
938
|
onProgress('android-app');
|
|
@@ -970,6 +1039,15 @@ async function setupExistingProject(projectId, options = {}) {
|
|
|
970
1039
|
error: storageResult.error,
|
|
971
1040
|
url: `https://console.firebase.google.com/project/${projectId}/storage`,
|
|
972
1041
|
});
|
|
1042
|
+
} else {
|
|
1043
|
+
onProgress('storage-cors');
|
|
1044
|
+
const corsResult = await applyStorageCors(projectId);
|
|
1045
|
+
if (!corsResult.ok) {
|
|
1046
|
+
onProgress('storage-cors-warn', {
|
|
1047
|
+
error: corsResult.error,
|
|
1048
|
+
url: `https://console.cloud.google.com/storage/browser?project=${projectId}`,
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
973
1051
|
}
|
|
974
1052
|
|
|
975
1053
|
return {
|
|
@@ -1033,6 +1111,7 @@ async function registerDebugSha1(projectId, bundleId) {
|
|
|
1033
1111
|
module.exports = {
|
|
1034
1112
|
setupFromScratch,
|
|
1035
1113
|
setupExistingProject,
|
|
1114
|
+
applyStorageCors,
|
|
1036
1115
|
checkBillingEnabled,
|
|
1037
1116
|
enableAuthProviders,
|
|
1038
1117
|
listBillingAccounts,
|
package/lib/utils/brand.js
CHANGED
|
@@ -19,7 +19,7 @@ const boxen = boxenPackage.default || boxenPackage;
|
|
|
19
19
|
//
|
|
20
20
|
// kleur 4 in this repo doesn't ship `.hex()`, so we emit 24-bit ANSI directly.
|
|
21
21
|
// Truecolor is supported by every macOS/Linux terminal we care about.
|
|
22
|
-
const BRAND_RGB = { r:
|
|
22
|
+
const BRAND_RGB = { r: 132, g: 204, b: 22 }; // #84CC16 lime (Tailwind lime-500, balanced for dark and light terminals)
|
|
23
23
|
const paintLime = (text) => `\x1b[38;2;${BRAND_RGB.r};${BRAND_RGB.g};${BRAND_RGB.b}m${text}\x1b[0m`;
|
|
24
24
|
const wordmark = paintLime;
|
|
25
25
|
const DOMAIN_SUFFIX = kleur.gray('.dev');
|
|
@@ -575,6 +575,10 @@ module.exports = {
|
|
|
575
575
|
'new.firebase.confirm.modules': 'Features',
|
|
576
576
|
'new.firebase.confirm.none': 'none',
|
|
577
577
|
'new.firebase.confirm.proceed': 'Create project now?',
|
|
578
|
+
'new.advanced.section.config': 'App configuration',
|
|
579
|
+
'new.advanced.section.features': 'Features',
|
|
580
|
+
'new.advanced.section.creds': 'Credentials',
|
|
581
|
+
'new.advanced.q.configureCredsNow': 'Enter credentials now? (you can defer to `kasy configure` later)',
|
|
578
582
|
|
|
579
583
|
'new.firebase.step.copying': 'Setting up your project...',
|
|
580
584
|
'new.firebase.step.pubGet': 'Installing Flutter packages...',
|
|
@@ -575,6 +575,10 @@ module.exports = {
|
|
|
575
575
|
'new.firebase.confirm.modules': 'Features',
|
|
576
576
|
'new.firebase.confirm.none': 'ninguno',
|
|
577
577
|
'new.firebase.confirm.proceed': '¿Crear el proyecto ahora?',
|
|
578
|
+
'new.advanced.section.config': 'Configuración del app',
|
|
579
|
+
'new.advanced.section.features': 'Funcionalidades',
|
|
580
|
+
'new.advanced.section.creds': 'Credenciales',
|
|
581
|
+
'new.advanced.q.configureCredsNow': '¿Configurar credenciales ahora? (puedes dejarlo para `kasy configure`)',
|
|
578
582
|
|
|
579
583
|
'new.firebase.step.copying': 'Copiando plantilla del proyecto...',
|
|
580
584
|
'new.firebase.step.pubGet': 'Instalando paquetes Flutter...',
|
|
@@ -575,6 +575,10 @@ module.exports = {
|
|
|
575
575
|
'new.firebase.confirm.modules': 'Features',
|
|
576
576
|
'new.firebase.confirm.none': 'nenhum',
|
|
577
577
|
'new.firebase.confirm.proceed': 'Criar o projeto agora?',
|
|
578
|
+
'new.advanced.section.config': 'Configuração do app',
|
|
579
|
+
'new.advanced.section.features': 'Funcionalidades',
|
|
580
|
+
'new.advanced.section.creds': 'Credenciais',
|
|
581
|
+
'new.advanced.q.configureCredsNow': 'Configurar credenciais agora? (pode deixar pra depois com `kasy configure`)',
|
|
578
582
|
|
|
579
583
|
'new.firebase.step.copying': 'Criando seu projeto...',
|
|
580
584
|
'new.firebase.step.pubGet': 'Instalando pacotes Flutter...',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kasy-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.19.0",
|
|
4
4
|
"description": "CLI for scaffolding production-ready Flutter SaaS apps with Firebase, Supabase, or API REST backends.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"kasy": "./bin/kasy.js"
|
|
@@ -51,7 +51,6 @@
|
|
|
51
51
|
"boxen": "^8.0.1",
|
|
52
52
|
"commander": "^12.0.0",
|
|
53
53
|
"fs-extra": "^11.2.0",
|
|
54
|
-
"gradient-string": "^1.2.0",
|
|
55
54
|
"kleur": "^4.1.5",
|
|
56
55
|
"pngjs": "^7.0.0",
|
|
57
56
|
"yaml": "^2.4.2"
|