kasy-cli 1.16.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 +16 -2
- package/lib/commands/add.js +52 -19
- package/lib/commands/configure.js +548 -0
- package/lib/commands/deploy.js +4 -4
- package/lib/commands/doctor.js +54 -6
- package/lib/commands/favicon.js +4 -4
- package/lib/commands/icon.js +5 -5
- package/lib/commands/new.js +404 -213
- package/lib/commands/remove.js +14 -3
- package/lib/commands/run.js +208 -6
- package/lib/commands/splash.js +5 -5
- package/lib/commands/update.js +9 -9
- package/lib/scaffold/CHANGELOG.json +23 -0
- package/lib/scaffold/backends/api/patch/README.md +3 -2
- 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/backends/supabase/patch/README.md +3 -2
- package/lib/scaffold/generate.js +24 -8
- package/lib/scaffold/shared/generator-utils.js +52 -8
- package/lib/scaffold/shared/post-build.js +113 -31
- package/lib/scaffold/shared/template-strings.js +6 -0
- package/lib/utils/brand.js +16 -12
- package/lib/utils/flutter-run.js +139 -11
- package/lib/utils/i18n/messages-en.js +85 -7
- package/lib/utils/i18n/messages-es.js +85 -7
- package/lib/utils/i18n/messages-pt.js +86 -8
- package/lib/utils/ui.js +79 -4
- package/package.json +1 -1
- package/templates/firebase/README.en.md +18 -8
- package/templates/firebase/README.es.md +18 -8
- package/templates/firebase/README.md +18 -8
- package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/MyWidget.kt +68 -45
- package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/MyWidgetReceiver.kt +37 -0
- package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/OpenAppAction.kt +26 -0
- 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/docs/revenuecat-setup.es.md +28 -8
- package/templates/firebase/docs/revenuecat-setup.pt.md +28 -8
- 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 +45 -53
- 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/lib/router.dart +15 -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/scaffold/generate.js
CHANGED
|
@@ -66,7 +66,7 @@ const {
|
|
|
66
66
|
removeDevelopmentTeam,
|
|
67
67
|
localizeReleaseDocs,
|
|
68
68
|
} = require('./shared/generator-utils');
|
|
69
|
-
const { pubGet, slangGenerate, buildRunner, flutterfireConfigure, writeGoogleAuthOptions, writeGoogleIosUrlScheme, patchFirebaseServiceWorker } = require('./shared/post-build');
|
|
69
|
+
const { pubGet, slangGenerate, buildRunner, dartFix, flutterfireConfigure, writeGoogleAuthOptions, writeGoogleIosUrlScheme, patchFirebaseServiceWorker } = require('./shared/post-build');
|
|
70
70
|
const { FIREBASE_SOURCE_DIR } = require('./shared/backend-config');
|
|
71
71
|
|
|
72
72
|
/**
|
|
@@ -97,6 +97,7 @@ async function generateProject(targetDir, backend, options, hooks = {}) {
|
|
|
97
97
|
onProgress = () => {},
|
|
98
98
|
includeWeb = true,
|
|
99
99
|
language = 'en',
|
|
100
|
+
deferGoogleAuthPatches = false,
|
|
100
101
|
} = options;
|
|
101
102
|
|
|
102
103
|
const { applyBackendSetup = null, postBuild = null } = hooks;
|
|
@@ -325,7 +326,12 @@ async function generateProject(targetDir, backend, options, hooks = {}) {
|
|
|
325
326
|
// Android: populate kGoogleWebClientId in lib/google_auth_options.dart.
|
|
326
327
|
// Skip for Supabase — it defines kGoogleIosClientId too, handled separately
|
|
327
328
|
// in new.js after credentials are resolved (readSupabaseGoogleCredentials).
|
|
328
|
-
|
|
329
|
+
//
|
|
330
|
+
// When `deferGoogleAuthPatches` is set, the caller will re-run flutterfire
|
|
331
|
+
// and these patches AFTER enabling Google Sign-In (which creates the OAuth
|
|
332
|
+
// Web Client + REVERSED_CLIENT_ID). Running them here would fail because the
|
|
333
|
+
// IDs don't exist yet, surfacing scary red errors that look like real bugs.
|
|
334
|
+
if (backend !== 'supabase' && !deferGoogleAuthPatches) {
|
|
329
335
|
const gaResult = await writeGoogleAuthOptions(targetDir);
|
|
330
336
|
steps.push({
|
|
331
337
|
name: 'google-auth-options',
|
|
@@ -335,12 +341,14 @@ async function generateProject(targetDir, backend, options, hooks = {}) {
|
|
|
335
341
|
}
|
|
336
342
|
|
|
337
343
|
// iOS: register REVERSED_CLIENT_ID as a URL scheme in ios/Runner/Info.plist
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
+
if (!deferGoogleAuthPatches) {
|
|
345
|
+
const iosSchemeResult = await writeGoogleIosUrlScheme(targetDir);
|
|
346
|
+
steps.push({
|
|
347
|
+
name: 'google-ios-url-scheme',
|
|
348
|
+
ok: iosSchemeResult.ok,
|
|
349
|
+
detail: iosSchemeResult.ok ? null : iosSchemeResult.error,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
344
352
|
|
|
345
353
|
// Web: patch firebase-messaging-sw.js with real config values (only when web module is selected)
|
|
346
354
|
if (modules.includes('web')) {
|
|
@@ -364,6 +372,14 @@ async function generateProject(targetDir, backend, options, hooks = {}) {
|
|
|
364
372
|
}
|
|
365
373
|
}
|
|
366
374
|
|
|
375
|
+
// ── 4. Auto-fix lints (directives_ordering, unused_import, etc.) ───────────
|
|
376
|
+
// Runs last so generated code, patches and post-build edits are all covered.
|
|
377
|
+
// Non-fatal: if it fails we still ship a working project; the user can run
|
|
378
|
+
// `dart fix --apply` manually later.
|
|
379
|
+
onProgress('dart-fix');
|
|
380
|
+
const dartFixResult = await dartFix(targetDir);
|
|
381
|
+
steps.push({ name: 'dart-fix', ok: dartFixResult.ok, detail: dartFixResult.ok ? null : dartFixResult.error });
|
|
382
|
+
|
|
367
383
|
return { steps, packageName, appName, bundleId, firebaseProjectId, ...returnExtra };
|
|
368
384
|
}
|
|
369
385
|
|
|
@@ -13,6 +13,36 @@ const pkg = require('../../../package.json');
|
|
|
13
13
|
/** Backend IDs: firebase | supabase | api */
|
|
14
14
|
const BACKENDS = Object.freeze(['firebase', 'supabase', 'api']);
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Resolve the default RC keys to bake into launch.json / Makefile.
|
|
18
|
+
*
|
|
19
|
+
* `kasy run` overrides these at runtime based on the device picked (test_ for
|
|
20
|
+
* simulator/emulator, appl_/goog_ for physical). The values here are only used
|
|
21
|
+
* when the user runs Flutter directly (e.g. F5 in VS Code, `make run`) without
|
|
22
|
+
* going through `kasy run`. We default to test_ since that's what works in the
|
|
23
|
+
* iOS Simulator and Android Emulator — the common dev case.
|
|
24
|
+
*
|
|
25
|
+
* Legacy support: projects generated before this refactor only ask for a single
|
|
26
|
+
* Android/iOS key (`rcAndroidKey` / `rcIosKey`). When those are passed we honor
|
|
27
|
+
* them as-is.
|
|
28
|
+
*/
|
|
29
|
+
function resolveDefaultRcKeys(answers) {
|
|
30
|
+
// Legacy single-key format (pre-test/prod split): keep behavior unchanged.
|
|
31
|
+
if ((answers.rcAndroidKey || answers.rcIosKey) && !answers.rcTestKey && !answers.rcIosProdKey && !answers.rcAndroidProdKey) {
|
|
32
|
+
return {
|
|
33
|
+
android: answers.rcAndroidKey || 'YOUR_REVENUECAT_ANDROID_KEY',
|
|
34
|
+
ios: answers.rcIosKey || 'YOUR_REVENUECAT_IOS_KEY',
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const test = (answers.rcTestKey || '').trim();
|
|
38
|
+
const iosProd = (answers.rcIosProdKey || '').trim();
|
|
39
|
+
const androidProd = (answers.rcAndroidProdKey || '').trim();
|
|
40
|
+
return {
|
|
41
|
+
android: test || androidProd || 'YOUR_REVENUECAT_ANDROID_KEY',
|
|
42
|
+
ios: test || iosProd || 'YOUR_REVENUECAT_IOS_KEY',
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
16
46
|
/**
|
|
17
47
|
* Build dart-define args for dev and prod environments.
|
|
18
48
|
* Backend-specific vars: Firebase (none extra), Supabase (BACKEND_URL, SUPABASE_TOKEN), API (BACKEND_URL).
|
|
@@ -40,12 +70,11 @@ function buildDartDefines(backend, modules, answers) {
|
|
|
40
70
|
}
|
|
41
71
|
|
|
42
72
|
if (modules.includes('revenuecat')) {
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
dev.push(`--dart-define=
|
|
46
|
-
|
|
47
|
-
prod.push(`--dart-define=
|
|
48
|
-
prod.push(`--dart-define=RC_IOS_API_KEY=${iosKey}`);
|
|
73
|
+
const { android: androidDefault, ios: iosDefault } = resolveDefaultRcKeys(answers);
|
|
74
|
+
dev.push(`--dart-define=RC_ANDROID_API_KEY=${androidDefault}`);
|
|
75
|
+
dev.push(`--dart-define=RC_IOS_API_KEY=${iosDefault}`);
|
|
76
|
+
prod.push(`--dart-define=RC_ANDROID_API_KEY=${androidDefault}`);
|
|
77
|
+
prod.push(`--dart-define=RC_IOS_API_KEY=${iosDefault}`);
|
|
49
78
|
if (answers.revenuecatWeb) {
|
|
50
79
|
const webKey = answers.rcWebKey || 'YOUR_REVENUECAT_WEB_KEY';
|
|
51
80
|
dev.push(`--dart-define=RC_WEB_API_KEY=${webKey}`);
|
|
@@ -144,8 +173,11 @@ async function writeEnvExample(projectDir, modules, answers, language = 'en') {
|
|
|
144
173
|
if (modules.includes('revenuecat')) {
|
|
145
174
|
lines.push('');
|
|
146
175
|
lines.push(t.revenuecat);
|
|
147
|
-
lines.push(
|
|
148
|
-
lines.push(`
|
|
176
|
+
if (t.revenuecatTest) lines.push(t.revenuecatTest);
|
|
177
|
+
lines.push(`RC_TEST_KEY=${(answers.rcTestKey || '').trim()}`);
|
|
178
|
+
if (t.revenuecatProd) lines.push(t.revenuecatProd);
|
|
179
|
+
lines.push(`RC_IOS_PROD_KEY=${(answers.rcIosProdKey || '').trim()}`);
|
|
180
|
+
lines.push(`RC_ANDROID_PROD_KEY=${(answers.rcAndroidProdKey || '').trim()}`);
|
|
149
181
|
if (answers.revenuecatWeb) {
|
|
150
182
|
lines.push(`RC_WEB_API_KEY=${answers.rcWebKey || 'YOUR_REVENUECAT_WEB_KEY'}`);
|
|
151
183
|
}
|
|
@@ -468,6 +500,16 @@ const bool revenuecatWeb = ${revenuecatWeb};
|
|
|
468
500
|
async function writeKitSetup(projectDir, options) {
|
|
469
501
|
const { appName, bundleId, backend, modules = [], firebaseProjectId, supabaseUrl, supabaseAnonKey, moduleAnswers = {} } = options;
|
|
470
502
|
const revenuecatWeb = !!(modules.includes('revenuecat') && modules.includes('web') && moduleAnswers.revenuecatWeb);
|
|
503
|
+
// Booleans tracking which RC keys the user configured. We never persist key
|
|
504
|
+
// values to kit_setup.json — those live in .env (gitignored). These flags
|
|
505
|
+
// let `kasy doctor` warn about release readiness without re-reading .env.
|
|
506
|
+
const revenuecatKeys = modules.includes('revenuecat')
|
|
507
|
+
? {
|
|
508
|
+
test: !!(moduleAnswers.rcTestKey && moduleAnswers.rcTestKey.trim()),
|
|
509
|
+
iosProd: !!(moduleAnswers.rcIosProdKey && moduleAnswers.rcIosProdKey.trim()),
|
|
510
|
+
androidProd: !!(moduleAnswers.rcAndroidProdKey && moduleAnswers.rcAndroidProdKey.trim()),
|
|
511
|
+
}
|
|
512
|
+
: undefined;
|
|
471
513
|
const config = {
|
|
472
514
|
appName: appName || 'App',
|
|
473
515
|
bundleId: bundleId || 'com.example.app',
|
|
@@ -480,6 +522,7 @@ async function writeKitSetup(projectDir, options) {
|
|
|
480
522
|
storageProvider: backend === 'firebase' ? 'firebase' : backend === 'supabase' ? 'supabase' : 'api',
|
|
481
523
|
webCompat: modules.includes('web'),
|
|
482
524
|
revenuecatWeb,
|
|
525
|
+
...(revenuecatKeys ? { revenuecatKeys } : {}),
|
|
483
526
|
internationalization: true,
|
|
484
527
|
useSentry: modules.includes('sentry'),
|
|
485
528
|
withOnboarding: modules.includes('onboarding'),
|
|
@@ -1638,6 +1681,7 @@ async function localizeReleaseDocs(projectDir, language = 'en') {
|
|
|
1638
1681
|
module.exports = {
|
|
1639
1682
|
BACKENDS,
|
|
1640
1683
|
buildDartDefines,
|
|
1684
|
+
resolveDefaultRcKeys,
|
|
1641
1685
|
writeRouter,
|
|
1642
1686
|
writeVsCodeLaunch,
|
|
1643
1687
|
writeEnvExample,
|
|
@@ -45,6 +45,13 @@ async function buildRunner(projectDir) {
|
|
|
45
45
|
return run('dart run build_runner build --delete-conflicting-outputs', projectDir, 600_000); // 10 min
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
// Auto-applies fixable lints (directives_ordering, unused_import, etc.) so the
|
|
49
|
+
// generated project opens in the user's IDE with zero warnings. Runs after
|
|
50
|
+
// build_runner so generated files are reordered too where applicable.
|
|
51
|
+
async function dartFix(projectDir) {
|
|
52
|
+
return run('dart fix --apply', projectDir, 180_000); // 3 min
|
|
53
|
+
}
|
|
54
|
+
|
|
48
55
|
async function flutterfireConfigure(projectDir, firebaseProjectId, options = {}) {
|
|
49
56
|
const { includeWeb = true } = options;
|
|
50
57
|
const platforms = includeWeb ? 'android,ios,web' : 'android,ios';
|
|
@@ -665,49 +672,123 @@ async function validateFacebookAndroidStrings(projectDir) {
|
|
|
665
672
|
}
|
|
666
673
|
|
|
667
674
|
/**
|
|
668
|
-
*
|
|
669
|
-
*
|
|
675
|
+
* Read a project `.env` file into a plain object. Returns `{}` if the file is
|
|
676
|
+
* missing. Supports KEY=VALUE lines; ignores comments and blank lines.
|
|
670
677
|
*/
|
|
671
|
-
async function
|
|
672
|
-
const
|
|
673
|
-
if (!(await fs.pathExists(
|
|
674
|
-
return { ok: true, skipped: true, reason: 'no_makefile' };
|
|
675
|
-
}
|
|
676
|
-
|
|
678
|
+
async function readDotenv(projectDir) {
|
|
679
|
+
const envPath = path.join(projectDir, '.env');
|
|
680
|
+
if (!(await fs.pathExists(envPath))) return {};
|
|
677
681
|
let content;
|
|
678
682
|
try {
|
|
679
|
-
content = await fs.readFile(
|
|
680
|
-
} catch
|
|
681
|
-
return {
|
|
683
|
+
content = await fs.readFile(envPath, 'utf8');
|
|
684
|
+
} catch {
|
|
685
|
+
return {};
|
|
686
|
+
}
|
|
687
|
+
const env = {};
|
|
688
|
+
for (const rawLine of content.split('\n')) {
|
|
689
|
+
const line = rawLine.trim();
|
|
690
|
+
if (!line || line.startsWith('#')) continue;
|
|
691
|
+
const eq = line.indexOf('=');
|
|
692
|
+
if (eq === -1) continue;
|
|
693
|
+
const key = line.slice(0, eq).trim();
|
|
694
|
+
let value = line.slice(eq + 1).trim();
|
|
695
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
696
|
+
value = value.slice(1, -1);
|
|
697
|
+
}
|
|
698
|
+
if (key) env[key] = value;
|
|
682
699
|
}
|
|
700
|
+
return env;
|
|
701
|
+
}
|
|
683
702
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
const iosTest = iosKey.startsWith('test_');
|
|
697
|
-
const androidTest = androidKey.startsWith('test_');
|
|
703
|
+
/**
|
|
704
|
+
* Validate RevenueCat keys. The new `.env` is the source of truth:
|
|
705
|
+
* RC_TEST_KEY (test_xxx — simulator/emulator)
|
|
706
|
+
* RC_IOS_PROD_KEY (appl_xxx — physical iOS)
|
|
707
|
+
* RC_ANDROID_PROD_KEY (goog_xxx — physical Android)
|
|
708
|
+
*
|
|
709
|
+
* Legacy fallback: if none of the new vars are set we read the old
|
|
710
|
+
* RC_ANDROID_API_KEY/RC_IOS_API_KEY from `.env`, and finally the Makefile
|
|
711
|
+
* (oldest projects). Returns flags `doctor.js` uses to render granular hints.
|
|
712
|
+
*/
|
|
713
|
+
async function validateRevenueCat(projectDir, config = {}) {
|
|
714
|
+
const env = await readDotenv(projectDir);
|
|
698
715
|
|
|
716
|
+
// Webhook URL (only Supabase exposes it directly; Firebase points to Console).
|
|
699
717
|
let webhookUrl = null;
|
|
700
718
|
if (config.backendProvider === 'supabase' && config.supabaseProjectId) {
|
|
701
719
|
webhookUrl = `https://${config.supabaseProjectId}.supabase.co/functions/v1/revenuecat-webhook`;
|
|
702
720
|
}
|
|
703
721
|
|
|
722
|
+
const testKey = (env.RC_TEST_KEY || '').trim();
|
|
723
|
+
const iosProdKey = (env.RC_IOS_PROD_KEY || '').trim();
|
|
724
|
+
const androidProdKey = (env.RC_ANDROID_PROD_KEY || '').trim();
|
|
725
|
+
const hasNewKeys = !!(testKey || iosProdKey || androidProdKey);
|
|
726
|
+
|
|
727
|
+
// Legacy single-key path (for projects generated before the test/prod split).
|
|
728
|
+
if (!hasNewKeys) {
|
|
729
|
+
let iosKey = (env.RC_IOS_API_KEY || '').trim();
|
|
730
|
+
let androidKey = (env.RC_ANDROID_API_KEY || '').trim();
|
|
731
|
+
|
|
732
|
+
// Last resort: read from Makefile (oldest projects).
|
|
733
|
+
if (!iosKey && !androidKey) {
|
|
734
|
+
const makefilePath = path.join(projectDir, 'Makefile');
|
|
735
|
+
if (await fs.pathExists(makefilePath)) {
|
|
736
|
+
try {
|
|
737
|
+
const content = await fs.readFile(makefilePath, 'utf8');
|
|
738
|
+
const activeLines = content.split('\n').filter((l) => !/^\s*#/.test(l)).join('\n');
|
|
739
|
+
const iosMatch = activeLines.match(/RC_IOS_API_KEY=([^\s\\]+)/);
|
|
740
|
+
const androidMatch = activeLines.match(/RC_ANDROID_API_KEY=([^\s\\]+)/);
|
|
741
|
+
iosKey = iosMatch ? iosMatch[1].trim() : '';
|
|
742
|
+
androidKey = androidMatch ? androidMatch[1].trim() : '';
|
|
743
|
+
} catch { /* ignore */ }
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
const iosEmpty = !iosKey || iosKey === 'xxx';
|
|
748
|
+
const androidEmpty = !androidKey || androidKey === 'xxx';
|
|
749
|
+
const iosTest = iosKey.startsWith('test_');
|
|
750
|
+
const androidTest = androidKey.startsWith('test_');
|
|
751
|
+
|
|
752
|
+
return {
|
|
753
|
+
ok: !iosEmpty && !androidEmpty,
|
|
754
|
+
legacy: true,
|
|
755
|
+
iosKey,
|
|
756
|
+
androidKey,
|
|
757
|
+
iosEmpty,
|
|
758
|
+
androidEmpty,
|
|
759
|
+
bothTest: iosTest && androidTest,
|
|
760
|
+
webhookUrl,
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// New 3-key scheme.
|
|
765
|
+
const testOk = !!testKey && /^test_/.test(testKey);
|
|
766
|
+
const testBadPrefix = !!testKey && !/^test_/.test(testKey);
|
|
767
|
+
const iosProdOk = !!iosProdKey && /^appl_/.test(iosProdKey);
|
|
768
|
+
const iosProdBadPrefix = !!iosProdKey && !/^appl_/.test(iosProdKey);
|
|
769
|
+
const androidProdOk = !!androidProdKey && /^goog_/.test(androidProdKey);
|
|
770
|
+
const androidProdBadPrefix = !!androidProdKey && !/^goog_/.test(androidProdKey);
|
|
771
|
+
|
|
772
|
+
// OK if user can run at least one device flow end-to-end. Test alone is fine
|
|
773
|
+
// for development; prod-only configurations also work but skip the simulator.
|
|
774
|
+
const anyOk = testOk || iosProdOk || androidProdOk;
|
|
775
|
+
|
|
704
776
|
return {
|
|
705
|
-
ok:
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
777
|
+
ok: anyOk,
|
|
778
|
+
legacy: false,
|
|
779
|
+
testKey,
|
|
780
|
+
iosProdKey,
|
|
781
|
+
androidProdKey,
|
|
782
|
+
testOk,
|
|
783
|
+
testBadPrefix,
|
|
784
|
+
iosProdOk,
|
|
785
|
+
iosProdBadPrefix,
|
|
786
|
+
androidProdOk,
|
|
787
|
+
androidProdBadPrefix,
|
|
788
|
+
testMissing: !testKey,
|
|
789
|
+
iosProdMissing: !iosProdKey,
|
|
790
|
+
androidProdMissing: !androidProdKey,
|
|
791
|
+
onlyTest: testOk && !iosProdOk && !androidProdOk,
|
|
711
792
|
webhookUrl,
|
|
712
793
|
};
|
|
713
794
|
}
|
|
@@ -787,6 +868,7 @@ module.exports = {
|
|
|
787
868
|
pubGet,
|
|
788
869
|
slangGenerate,
|
|
789
870
|
buildRunner,
|
|
871
|
+
dartFix,
|
|
790
872
|
flutterfireConfigure,
|
|
791
873
|
writeGoogleAuthOptions,
|
|
792
874
|
writeGoogleIosUrlScheme,
|
|
@@ -33,6 +33,8 @@ const TEMPLATE_STRINGS = {
|
|
|
33
33
|
supabase: '# Supabase',
|
|
34
34
|
apiRest: '# API REST',
|
|
35
35
|
revenuecat: '# RevenueCat',
|
|
36
|
+
revenuecatTest: '# Test Store key (test_xxx) — auto-used on simulator/emulator',
|
|
37
|
+
revenuecatProd: '# Production keys — auto-used on physical devices',
|
|
36
38
|
sentry: '# Sentry (prod only)',
|
|
37
39
|
mixpanel: '# Mixpanel',
|
|
38
40
|
llmChat: '# LLM Chat — Cloud/Edge Function endpoint (API key stays on the server, not here)',
|
|
@@ -65,6 +67,8 @@ const TEMPLATE_STRINGS = {
|
|
|
65
67
|
supabase: '# Supabase',
|
|
66
68
|
apiRest: '# API REST',
|
|
67
69
|
revenuecat: '# RevenueCat',
|
|
70
|
+
revenuecatTest: '# Chave Test Store (test_xxx) — usada automaticamente em simulador/emulador',
|
|
71
|
+
revenuecatProd: '# Chaves de produção — usadas automaticamente em dispositivo físico',
|
|
68
72
|
sentry: '# Sentry (apenas prod)',
|
|
69
73
|
mixpanel: '# Mixpanel',
|
|
70
74
|
llmChat: '# LLM Chat — endpoint da Cloud/Edge Function (a chave de API fica no servidor, não aqui)',
|
|
@@ -97,6 +101,8 @@ const TEMPLATE_STRINGS = {
|
|
|
97
101
|
supabase: '# Supabase',
|
|
98
102
|
apiRest: '# API REST',
|
|
99
103
|
revenuecat: '# RevenueCat',
|
|
104
|
+
revenuecatTest: '# Clave Test Store (test_xxx) — se usa automáticamente en simulador/emulador',
|
|
105
|
+
revenuecatProd: '# Claves de producción — se usan automáticamente en dispositivo físico',
|
|
100
106
|
sentry: '# Sentry (solo prod)',
|
|
101
107
|
mixpanel: '# Mixpanel',
|
|
102
108
|
llmChat: '# LLM Chat — endpoint de Cloud/Edge Function (la clave de API queda en el servidor, no aquí)',
|
package/lib/utils/brand.js
CHANGED
|
@@ -10,22 +10,26 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
const kleur = require('kleur');
|
|
13
|
-
const gradient = require('gradient-string');
|
|
14
13
|
const boxenPackage = require('boxen');
|
|
15
14
|
const boxen = boxenPackage.default || boxenPackage;
|
|
16
15
|
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
// Brand palette (memory/kasy_brand_colors.md). Single source of truth for the
|
|
17
|
+
// CLI's only brand color: change BRAND_RGB and every spinner, header, logo and
|
|
18
|
+
// success card across all commands updates automatically.
|
|
19
|
+
//
|
|
20
|
+
// kleur 4 in this repo doesn't ship `.hex()`, so we emit 24-bit ANSI directly.
|
|
21
|
+
// Truecolor is supported by every macOS/Linux terminal we care about.
|
|
22
|
+
const BRAND_RGB = { r: 210, g: 245, b: 30 }; // #D2F51E lime
|
|
23
|
+
const paintLime = (text) => `\x1b[38;2;${BRAND_RGB.r};${BRAND_RGB.g};${BRAND_RGB.b}m${text}\x1b[0m`;
|
|
24
|
+
const wordmark = paintLime;
|
|
21
25
|
const DOMAIN_SUFFIX = kleur.gray('.dev');
|
|
22
26
|
|
|
23
27
|
function printBanner(_tr) {
|
|
24
28
|
const bar = kleur.gray('─────────────────────────────────────────────────');
|
|
25
29
|
const logo = [
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
`${
|
|
30
|
+
wordmark(' ╦╔═ ╔═╗ ╔═╗ ╦ ╦'),
|
|
31
|
+
wordmark(' ╠╩╗ ╠═╣ ╚═╗ ╚╦╝'),
|
|
32
|
+
`${wordmark(' ╩ ╩ ╩ ╩ ╚═╝ ╩ ')} ${DOMAIN_SUFFIX}`,
|
|
29
33
|
].join('\n');
|
|
30
34
|
|
|
31
35
|
console.log(`\n${bar}\n`);
|
|
@@ -35,18 +39,18 @@ function printBanner(_tr) {
|
|
|
35
39
|
|
|
36
40
|
function printCompactHeader(_tr) {
|
|
37
41
|
console.log('');
|
|
38
|
-
console.log(` ${
|
|
42
|
+
console.log(` ${wordmark('✦ KASY')}${DOMAIN_SUFFIX}`);
|
|
39
43
|
console.log('');
|
|
40
44
|
}
|
|
41
45
|
|
|
42
46
|
function successBox(title, body, { padding = 1, marginTop = 1, marginBottom = 1 } = {}) {
|
|
43
47
|
return boxen(
|
|
44
|
-
`${
|
|
48
|
+
`${paintLime(`✦ ${title}`)}\n\n${body}`,
|
|
45
49
|
{
|
|
46
50
|
padding,
|
|
47
51
|
margin: { top: marginTop, bottom: marginBottom, left: 1, right: 1 },
|
|
48
52
|
borderStyle: 'round',
|
|
49
|
-
borderColor: '
|
|
53
|
+
borderColor: 'gray',
|
|
50
54
|
}
|
|
51
55
|
);
|
|
52
56
|
}
|
|
@@ -64,7 +68,7 @@ function infoBox(title, body, { padding = 1, marginTop = 1, marginBottom = 1 } =
|
|
|
64
68
|
}
|
|
65
69
|
|
|
66
70
|
module.exports = {
|
|
67
|
-
|
|
71
|
+
paintLime,
|
|
68
72
|
printBanner,
|
|
69
73
|
printCompactHeader,
|
|
70
74
|
successBox,
|
package/lib/utils/flutter-run.js
CHANGED
|
@@ -5,18 +5,29 @@
|
|
|
5
5
|
* stage detection, elapsed timer, hot-reload stdin pass-through and SIGINT
|
|
6
6
|
* forwarding stay consistent in one place.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* -
|
|
11
|
-
*
|
|
12
|
-
* stage marker (Gradle, Xcode, install, sync, etc.).
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
8
|
+
* Two modes:
|
|
9
|
+
*
|
|
10
|
+
* - spinner mode (default, TTY only): Spawn flutter with piped stdio. Show a
|
|
11
|
+
* Clack spinner with elapsed time. Update the spinner message when we see a
|
|
12
|
+
* known stage marker (Gradle, Xcode, install, sync, etc.). When Flutter
|
|
13
|
+
* signals "ready" (DAP key commands), flush buffered output, drop into stdio
|
|
14
|
+
* pass-through, and pipe stdin so hot reload works. On early exit, replay
|
|
15
|
+
* the buffer so the user can see the error.
|
|
16
|
+
*
|
|
17
|
+
* - raw mode (auto when stdout is not a TTY, or forced via `--raw`):
|
|
18
|
+
* No spinner, no buffer. Flutter output is piped straight through so devs
|
|
19
|
+
* and AI agents see the unmodified stream.
|
|
20
|
+
*
|
|
21
|
+
* In both modes we always tee the raw (ANSI-stripped) flutter output to
|
|
22
|
+
* `<project>/.kasy/run.log`. This gives AI assistants a stable, single-file
|
|
23
|
+
* source of truth for the last run, even when the spinner cosmetically rewrites
|
|
24
|
+
* the human terminal.
|
|
16
25
|
*/
|
|
17
26
|
|
|
18
27
|
'use strict';
|
|
19
28
|
|
|
29
|
+
const path = require('node:path');
|
|
30
|
+
const fs = require('node:fs');
|
|
20
31
|
const { spawn } = require('node:child_process');
|
|
21
32
|
const kleur = require('kleur');
|
|
22
33
|
const ui = require('./ui');
|
|
@@ -24,6 +35,35 @@ const ui = require('./ui');
|
|
|
24
35
|
// Markers that tell us the initial build is done and the app is running.
|
|
25
36
|
const FLUTTER_READY_RE = /Flutter run key commands\.|is listening on|VM Service|Dart VM service|To hot reload|Hot restart/i;
|
|
26
37
|
|
|
38
|
+
// Strip ANSI escape sequences before persisting to the log file so the file
|
|
39
|
+
// is grep-friendly for tooling (CI logs, agents, editors).
|
|
40
|
+
const ANSI_RE = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g;
|
|
41
|
+
|
|
42
|
+
function stripAnsi(text) {
|
|
43
|
+
return String(text).replace(ANSI_RE, '');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Open (and create the parent dir of) the persistent run log. Overwrites any
|
|
48
|
+
* previous run — we want "the last run", not an ever-growing history.
|
|
49
|
+
*
|
|
50
|
+
* Returns a write stream, or null when the file can't be created (read-only
|
|
51
|
+
* project, permissions, etc.) — failure here must never break `kasy run`.
|
|
52
|
+
*/
|
|
53
|
+
function openRunLog(projectDir) {
|
|
54
|
+
const logPath = path.join(projectDir, '.kasy', 'run.log');
|
|
55
|
+
try {
|
|
56
|
+
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
57
|
+
const stream = fs.createWriteStream(logPath, { flags: 'w' });
|
|
58
|
+
stream.on('error', () => {});
|
|
59
|
+
const header = `# kasy run — ${new Date().toISOString()}\n`;
|
|
60
|
+
stream.write(header);
|
|
61
|
+
return { stream, logPath };
|
|
62
|
+
} catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
27
67
|
/**
|
|
28
68
|
* Map a chunk of Flutter output to a user-friendly stage label, or null
|
|
29
69
|
* if the chunk doesn't carry any of the known markers.
|
|
@@ -66,10 +106,82 @@ function render(stageMessage, startTime) {
|
|
|
66
106
|
}
|
|
67
107
|
|
|
68
108
|
/**
|
|
69
|
-
*
|
|
70
|
-
*
|
|
109
|
+
* Pretty-print the path used for the run log. Falls back to the absolute path
|
|
110
|
+
* when the log lives outside cwd (unusual, but keeps the message accurate).
|
|
71
111
|
*/
|
|
72
|
-
function
|
|
112
|
+
function relLogPath(logPath) {
|
|
113
|
+
const rel = path.relative(process.cwd(), logPath);
|
|
114
|
+
return rel.startsWith('..') ? logPath : rel;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Spawn `flutter` with the given args inside `projectDir`. When `options.raw`
|
|
119
|
+
* is true or stdout is not a TTY, runs in raw pass-through mode. Otherwise,
|
|
120
|
+
* shows a Clack spinner that buffers output until Flutter is ready.
|
|
121
|
+
*
|
|
122
|
+
* Resolves on success, rejects on non-zero exit. The log file path is always
|
|
123
|
+
* announced at the end so callers (and AI agents) can find the full output.
|
|
124
|
+
*/
|
|
125
|
+
function spawnFlutterWithSpinner(args, projectDir, t, options = {}) {
|
|
126
|
+
const raw = Boolean(options.raw) || !process.stdout.isTTY;
|
|
127
|
+
const log = openRunLog(projectDir);
|
|
128
|
+
|
|
129
|
+
if (raw) return spawnRaw(args, projectDir, t, log);
|
|
130
|
+
return spawnWithSpinner(args, projectDir, t, log);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Raw mode: no spinner, no buffer. Flutter stdout/stderr are piped straight
|
|
135
|
+
* through to the parent process, and tee'd (ANSI-stripped) into the log file.
|
|
136
|
+
*
|
|
137
|
+
* Stdin is forwarded directly so hot-reload keys (r/R/q) work without any
|
|
138
|
+
* raw-mode toggling — the user is already on a "dumb" terminal here.
|
|
139
|
+
*/
|
|
140
|
+
function spawnRaw(args, projectDir, t, log) {
|
|
141
|
+
return new Promise((resolve, reject) => {
|
|
142
|
+
const proc = spawn('flutter', args, {
|
|
143
|
+
cwd: projectDir,
|
|
144
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const tee = (chunk, sink) => {
|
|
148
|
+
sink.write(chunk);
|
|
149
|
+
if (log) log.stream.write(stripAnsi(chunk.toString()));
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
proc.stdout.on('data', (c) => tee(c, process.stdout));
|
|
153
|
+
proc.stderr.on('data', (c) => tee(c, process.stderr));
|
|
154
|
+
|
|
155
|
+
const sigintHandler = () => { try { proc.kill('SIGINT'); } catch (_) {} };
|
|
156
|
+
process.on('SIGINT', sigintHandler);
|
|
157
|
+
|
|
158
|
+
const cleanup = () => {
|
|
159
|
+
process.off('SIGINT', sigintHandler);
|
|
160
|
+
if (log) log.stream.end();
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
proc.on('close', (code) => {
|
|
164
|
+
cleanup();
|
|
165
|
+
announceLog(log, t);
|
|
166
|
+
if (code === 0) resolve();
|
|
167
|
+
else reject(new Error(`flutter exited with code ${code}`));
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
proc.on('error', (err) => {
|
|
171
|
+
cleanup();
|
|
172
|
+
announceLog(log, t);
|
|
173
|
+
reject(err);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Spinner mode: buffer flutter output during the build phase, surface progress
|
|
180
|
+
* as friendly stage labels, then flush + pass-through once Flutter signals
|
|
181
|
+
* ready. On early exit, the buffer is replayed so the user sees the real
|
|
182
|
+
* Flutter error.
|
|
183
|
+
*/
|
|
184
|
+
function spawnWithSpinner(args, projectDir, t, log) {
|
|
73
185
|
return new Promise((resolve, reject) => {
|
|
74
186
|
const proc = spawn('flutter', args, {
|
|
75
187
|
cwd: projectDir,
|
|
@@ -106,7 +218,12 @@ function spawnFlutterWithSpinner(args, projectDir, t) {
|
|
|
106
218
|
}
|
|
107
219
|
};
|
|
108
220
|
|
|
221
|
+
const teeLog = (chunk) => {
|
|
222
|
+
if (log) log.stream.write(stripAnsi(chunk.toString()));
|
|
223
|
+
};
|
|
224
|
+
|
|
109
225
|
const handleStdout = (chunk) => {
|
|
226
|
+
teeLog(chunk);
|
|
110
227
|
const text = chunk.toString();
|
|
111
228
|
if (!ready) {
|
|
112
229
|
buffer.push(chunk);
|
|
@@ -122,6 +239,7 @@ function spawnFlutterWithSpinner(args, projectDir, t) {
|
|
|
122
239
|
};
|
|
123
240
|
|
|
124
241
|
const handleStderr = (chunk) => {
|
|
242
|
+
teeLog(chunk);
|
|
125
243
|
if (!ready) {
|
|
126
244
|
buffer.push(chunk);
|
|
127
245
|
if (FLUTTER_READY_RE.test(chunk.toString())) flushAndSwitch();
|
|
@@ -144,6 +262,7 @@ function spawnFlutterWithSpinner(args, projectDir, t) {
|
|
|
144
262
|
process.stdin.unpipe(proc.stdin);
|
|
145
263
|
process.stdin.pause();
|
|
146
264
|
}
|
|
265
|
+
if (log) log.stream.end();
|
|
147
266
|
};
|
|
148
267
|
|
|
149
268
|
proc.on('close', (code) => {
|
|
@@ -152,6 +271,7 @@ function spawnFlutterWithSpinner(args, projectDir, t) {
|
|
|
152
271
|
spinner.stop(t('run.spinner.failed'), 2);
|
|
153
272
|
for (const chunk of buffer) process.stdout.write(chunk);
|
|
154
273
|
}
|
|
274
|
+
announceLog(log, t);
|
|
155
275
|
if (code === 0) resolve();
|
|
156
276
|
else reject(new Error(`flutter exited with code ${code}`));
|
|
157
277
|
});
|
|
@@ -159,15 +279,23 @@ function spawnFlutterWithSpinner(args, projectDir, t) {
|
|
|
159
279
|
proc.on('error', (err) => {
|
|
160
280
|
cleanup();
|
|
161
281
|
if (!ready) spinner.stop(t('run.spinner.failed'), 2);
|
|
282
|
+
announceLog(log, t);
|
|
162
283
|
reject(err);
|
|
163
284
|
});
|
|
164
285
|
});
|
|
165
286
|
}
|
|
166
287
|
|
|
288
|
+
function announceLog(log, t) {
|
|
289
|
+
if (!log) return;
|
|
290
|
+
const msg = t('run.log.savedTo', { path: relLogPath(log.logPath) });
|
|
291
|
+
console.log(kleur.dim(`\n ${msg}`));
|
|
292
|
+
}
|
|
293
|
+
|
|
167
294
|
module.exports = {
|
|
168
295
|
spawnFlutterWithSpinner,
|
|
169
296
|
// Exported for tests and reuse in non-flutter spawn helpers.
|
|
170
297
|
detectStage,
|
|
171
298
|
formatElapsed,
|
|
172
299
|
FLUTTER_READY_RE,
|
|
300
|
+
stripAnsi,
|
|
173
301
|
};
|