kasy-cli 1.12.1 → 1.14.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 +143 -7
- package/lib/commands/add.js +2 -2
- package/lib/commands/codemagic.js +11 -4
- package/lib/commands/deploy.js +3 -3
- package/lib/commands/favicon.js +115 -0
- package/lib/commands/icon.js +143 -0
- package/lib/commands/ios.js +20 -5
- package/lib/commands/new.js +8 -20
- package/lib/commands/remove.js +1 -1
- package/lib/commands/reset.js +287 -0
- package/lib/commands/run.js +24 -17
- package/lib/commands/splash.js +219 -0
- package/lib/commands/update.js +1 -1
- package/lib/scaffold/CHANGELOG.json +9 -0
- package/lib/scaffold/backends/api/patch/README.md +1 -1
- package/lib/scaffold/backends/api/patch/android/app/src/main/AndroidManifest.xml +1 -1
- package/lib/scaffold/backends/api/patch/lib/features/notifications/api/device_api.dart +53 -0
- package/lib/scaffold/backends/api/patch/lib/main.dart +29 -10
- package/lib/scaffold/backends/api/pubspec.yaml.tpl +11 -1
- package/lib/scaffold/backends/firebase/tokens.js +2 -2
- package/lib/scaffold/backends/supabase/edge-functions/send-push-notification/index.ts +8 -2
- package/lib/scaffold/backends/supabase/migrations/20240101000011_dedupe_device_tokens.sql +34 -0
- package/lib/scaffold/backends/supabase/patch/README.md +1 -1
- package/lib/scaffold/backends/supabase/patch/android/app/src/main/AndroidManifest.xml +1 -1
- package/lib/scaffold/backends/supabase/patch/lib/features/notifications/api/device_api.dart +43 -0
- package/lib/scaffold/backends/supabase/patch/lib/main.dart +29 -10
- package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +11 -1
- package/lib/scaffold/features/README.md +15 -139
- package/lib/scaffold/shared/generator-utils.js +16 -15
- package/lib/utils/apple-release.js +85 -16
- package/lib/utils/checks.js +4 -105
- package/lib/utils/flutter-run.js +173 -0
- package/lib/utils/i18n.js +413 -0
- package/lib/utils/mobile-identity.js +35 -0
- package/lib/utils/ui.js +114 -0
- package/package.json +2 -3
- 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/build.gradle.kts +10 -1
- package/templates/firebase/android/app/src/main/AndroidManifest.xml +1 -1
- package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/MainActivity.kt +25 -1
- package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/MyWidget.kt +160 -11
- package/templates/firebase/android/app/src/main/res/drawable/widget_add_button.xml +15 -0
- package/templates/firebase/android/app/src/main/res/drawable/widget_gradient_bg.xml +9 -0
- package/templates/firebase/android/app/src/main/res/drawable/widget_gradient_inner.xml +12 -0
- package/templates/firebase/android/app/src/main/res/drawable/widget_plan_pill_bg.xml +5 -0
- package/templates/firebase/android/app/src/main/res/drawable/widget_preview_image.xml +17 -0
- package/templates/firebase/android/app/src/main/res/drawable/widget_pro_pill_bg.xml +5 -0
- 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/background.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night/launch_background.xml +9 -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-v21/background.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-v21/launch_background.xml +9 -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_loading.xml +8 -0
- package/templates/firebase/android/app/src/main/res/layout/widget_preview.xml +46 -0
- package/templates/firebase/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
- package/templates/firebase/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
- package/templates/firebase/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
- package/templates/firebase/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
- package/templates/firebase/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
- package/templates/firebase/android/app/src/main/res/values-night-v31/styles.xml +2 -1
- package/templates/firebase/android/app/src/main/res/values-v31/styles.xml +1 -0
- package/templates/firebase/android/app/src/main/res/xml/mywidget_info.xml +9 -3
- package/templates/firebase/assets/images/favicon.png +0 -0
- package/templates/firebase/assets/images/icon.png +0 -0
- package/templates/firebase/assets/images/splash_logo_dark.png +0 -0
- package/templates/firebase/assets/images/splash_logo_light.png +0 -0
- package/templates/firebase/firestore.indexes.json +10 -0
- package/templates/firebase/functions/src/core/data/entities/user_device_entity.ts +3 -0
- package/templates/firebase/functions/src/core/data/repositories/user_device_repository.ts +17 -1
- package/templates/firebase/functions/src/index.ts +1 -0
- package/templates/firebase/functions/src/notifications/device_triggers.ts +58 -0
- package/templates/firebase/ios/HomeWidgetExtension/MyWidget.swift +116 -33
- package/templates/firebase/ios/Runner/AppDelegate.swift +17 -1
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/LaunchBackground.imageset/Contents.json +9 -8
- package/templates/firebase/ios/Runner/Assets.xcassets/LaunchBackground.imageset/darkbackground.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json +33 -0
- 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/ios/Runner/Info.plist +2 -2
- package/templates/firebase/ios/Runner/es.lproj/InfoPlist.strings +1 -1
- package/templates/firebase/ios/Runner/pt-BR.lproj/InfoPlist.strings +1 -1
- package/templates/firebase/ios/Runner/pt.lproj/InfoPlist.strings +1 -1
- package/templates/firebase/lib/components/kasy_button.dart +8 -0
- package/templates/firebase/lib/core/bottom_menu/kasy_bottom_bar_factory.dart +18 -0
- package/templates/firebase/lib/core/home_widgets/home_widget_mywidget_service.dart +67 -19
- package/templates/firebase/lib/core/home_widgets/home_widget_service.dart +22 -8
- package/templates/firebase/lib/core/shared_preferences/shared_preferences.dart +16 -4
- package/templates/firebase/lib/core/states/components/maybeshow_component.dart +4 -8
- package/templates/firebase/lib/core/states/user_state_notifier.dart +13 -1
- package/templates/firebase/lib/core/theme/providers/theme_provider.dart +48 -24
- package/templates/firebase/lib/features/home/home_page.dart +0 -6
- package/templates/firebase/lib/features/notifications/api/device_api.dart +57 -0
- package/templates/firebase/lib/features/notifications/providers/models/notification.dart +11 -1
- package/templates/firebase/lib/features/notifications/repositories/device_repository.dart +9 -0
- package/templates/firebase/lib/features/notifications/repositories/notifications_repository.dart +1 -4
- package/templates/firebase/lib/features/notifications/shared/att_permission.dart +28 -8
- package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +16 -1
- package/templates/firebase/lib/features/notifications/ui/widgets/empty_notifications.dart +44 -11
- package/templates/firebase/lib/features/onboarding/ui/components/onboarding_features.dart +4 -0
- package/templates/firebase/lib/features/onboarding/ui/onboarding_page.dart +1 -0
- package/templates/firebase/lib/features/onboarding/ui/widgets/onboarding_feature.dart +13 -0
- package/templates/firebase/lib/features/settings/settings_page.dart +158 -18
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_bottom_sheet.dart +31 -29
- package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +21 -5
- package/templates/firebase/lib/i18n/en.i18n.json +10 -3
- package/templates/firebase/lib/i18n/es.i18n.json +10 -3
- package/templates/firebase/lib/i18n/pt.i18n.json +10 -3
- package/templates/firebase/lib/main.dart +29 -10
- package/templates/firebase/pubspec.yaml +10 -6
- package/templates/firebase/test/core/data/repositories/user_repository_test.dart +1 -1
- package/templates/firebase/test/features/notifications/data/device_api_fake.dart +9 -0
- package/templates/firebase/web/favicon.png +0 -0
- package/templates/firebase/web/icons/Icon-192.png +0 -0
- package/templates/firebase/web/icons/Icon-512.png +0 -0
- package/templates/firebase/web/icons/Icon-maskable-192.png +0 -0
- package/templates/firebase/web/icons/Icon-maskable-512.png +0 -0
- package/templates/firebase/web/index.html +50 -39
- package/templates/firebase/web/manifest.json +3 -3
- 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/features/analytics/lib/core/data/api/analytics_api.dart +0 -124
- package/lib/scaffold/features/ci/.github/ISSUE_TEMPLATE/BUG.es.md +0 -35
- package/lib/scaffold/features/ci/.github/ISSUE_TEMPLATE/BUG.md +0 -35
- package/lib/scaffold/features/ci/.github/ISSUE_TEMPLATE/BUG.pt.md +0 -35
- package/lib/scaffold/features/ci/.github/ISSUE_TEMPLATE/feature_request.es.md +0 -12
- package/lib/scaffold/features/ci/.github/ISSUE_TEMPLATE/feature_request.md +0 -12
- package/lib/scaffold/features/ci/.github/ISSUE_TEMPLATE/feature_request.pt.md +0 -12
- package/lib/scaffold/features/ci/.github/PULL_REQUEST_TEMPLATE.es.md +0 -17
- package/lib/scaffold/features/ci/.github/PULL_REQUEST_TEMPLATE.md +0 -17
- package/lib/scaffold/features/ci/.github/PULL_REQUEST_TEMPLATE.pt.md +0 -17
- package/lib/scaffold/features/ci/.github/dependabot.yml +0 -16
- package/lib/scaffold/features/ci/.github/workflows/app.yml +0 -20
- package/lib/scaffold/features/ci/.gitlab/templates/deploy.yaml +0 -14
- package/lib/scaffold/features/ci/.gitlab/templates/dropbox.yaml +0 -19
- package/lib/scaffold/features/ci/.gitlab/templates/flutter.yaml +0 -163
- package/lib/scaffold/features/ci/.gitlab/templates/mailgun.yaml +0 -28
- package/lib/scaffold/features/ci/.gitlab-ci.yml +0 -37
- package/lib/scaffold/features/ci/codemagic.yaml +0 -157
- package/lib/scaffold/features/facebook/lib/core/data/api/tracking_api.dart +0 -111
- package/lib/scaffold/features/feedback/lib/features/feedbacks/api/entities/feature_request_entity.dart +0 -27
- package/lib/scaffold/features/feedback/lib/features/feedbacks/api/entities/feature_vote_entity.dart +0 -27
- package/lib/scaffold/features/feedback/lib/features/feedbacks/api/feature_request_api.dart +0 -50
- package/lib/scaffold/features/feedback/lib/features/feedbacks/api/feature_vote_api.dart +0 -79
- package/lib/scaffold/features/feedback/lib/features/feedbacks/models/feature_requests.dart +0 -48
- package/lib/scaffold/features/feedback/lib/features/feedbacks/models/feedback_state.dart +0 -42
- package/lib/scaffold/features/feedback/lib/features/feedbacks/providers/feedback_page_notifier.dart +0 -147
- package/lib/scaffold/features/feedback/lib/features/feedbacks/repositories/feature_request_repository.dart +0 -95
- package/lib/scaffold/features/feedback/lib/features/feedbacks/ui/component/add_feature_form.dart +0 -199
- package/lib/scaffold/features/feedback/lib/features/feedbacks/ui/feedback_page.dart +0 -175
- package/lib/scaffold/features/feedback/lib/features/feedbacks/ui/widgets/add_feature_button.dart +0 -76
- package/lib/scaffold/features/feedback/lib/features/feedbacks/ui/widgets/feature_card.dart +0 -279
- package/lib/scaffold/features/ios-release/.kasy/apple.env.example +0 -8
- package/lib/scaffold/features/ios-release/.kasy/codemagic.env.example +0 -7
- package/lib/scaffold/features/ios-release/docs/codemagic-release.en.md +0 -50
- package/lib/scaffold/features/ios-release/docs/codemagic-release.es.md +0 -50
- package/lib/scaffold/features/ios-release/docs/codemagic-release.pt.md +0 -50
- package/lib/scaffold/features/ios-release/docs/ios-release.en.md +0 -41
- package/lib/scaffold/features/ios-release/docs/ios-release.es.md +0 -41
- package/lib/scaffold/features/ios-release/docs/ios-release.pt.md +0 -41
- package/lib/scaffold/features/ios-release/scripts/bump-ios-version.js +0 -38
- package/lib/scaffold/features/ios-release/scripts/release-ios.sh +0 -137
- package/lib/scaffold/features/llm_chat/lib/features/llm_chat/llm_chat_page.dart +0 -301
- package/lib/scaffold/features/local_notifications/lib/features/local_reminder/providers/reminder_notifier.dart +0 -81
- package/lib/scaffold/features/local_notifications/lib/features/local_reminder/repositories/reminder_preferences.dart +0 -76
- package/lib/scaffold/features/local_notifications/lib/features/local_reminder/ui/reminder_page.dart +0 -282
- package/lib/scaffold/features/onboarding/lib/features/onboarding/api/entities/user_info_entity.dart +0 -24
- package/lib/scaffold/features/onboarding/lib/features/onboarding/api/user_infos_api.dart +0 -71
- package/lib/scaffold/features/onboarding/lib/features/onboarding/models/user_info.dart +0 -92
- package/lib/scaffold/features/onboarding/lib/features/onboarding/providers/onboarding_model.dart +0 -15
- package/lib/scaffold/features/onboarding/lib/features/onboarding/providers/onboarding_provider.dart +0 -78
- package/lib/scaffold/features/onboarding/lib/features/onboarding/repositories/user_infos_repository.dart +0 -29
- package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/animations/page_transitions.dart +0 -30
- package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/components/onboarding_att_setup.dart +0 -66
- package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/components/onboarding_features.dart +0 -72
- package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/components/onboarding_loader.dart +0 -92
- package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/components/onboarding_notifications_setup.dart +0 -73
- package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/components/onboarding_questions.dart +0 -89
- package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/onboarding_page.dart +0 -94
- package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/widgets/onboarding_background.dart +0 -80
- package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/widgets/onboarding_feature.dart +0 -139
- package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/widgets/onboarding_illustration_scaffold.dart +0 -110
- package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/widgets/onboarding_progress.dart +0 -84
- package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/widgets/onboarding_radio_question.dart +0 -173
- package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/widgets/onboarding_reassurance.dart +0 -45
- package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/widgets/onboarding_sticky_footer.dart +0 -77
- package/lib/scaffold/features/onboarding/lib/features/onboarding/ui/widgets/selectable_row_tile.dart +0 -392
- package/lib/scaffold/features/revenuecat/lib/core/data/api/tracking_api.dart +0 -116
- package/lib/scaffold/features/revenuecat/lib/core/data/models/subscription.dart +0 -322
- package/lib/scaffold/features/revenuecat/lib/core/home_widgets/home_widget_background_task.dart +0 -41
- package/lib/scaffold/features/revenuecat/lib/core/states/user_state_notifier.dart +0 -305
- package/templates/firebase/assets/images/app_icon.png +0 -0
- package/templates/firebase/assets/images/onboarding/img1.jpg +0 -0
- package/templates/firebase/assets/images/onboarding/onboarding.png +0 -0
- package/templates/firebase/assets/images/splashscreen.png +0 -0
- package/templates/firebase/lib/core/states/components/maybe_ask_biometric_setup.dart +0 -88
- package/templates/firebase/lib/features/notifications/shared/notification_permission_bottom_sheet.dart +0 -144
package/lib/commands/new.js
CHANGED
|
@@ -15,22 +15,10 @@ const path = require('node:path');
|
|
|
15
15
|
const crypto = require('node:crypto');
|
|
16
16
|
const kleur = require('kleur');
|
|
17
17
|
|
|
18
|
-
function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
|
|
19
|
-
|
|
20
18
|
function generateWebhookKey() {
|
|
21
19
|
return 'rc_wh_' + crypto.randomBytes(16).toString('hex');
|
|
22
20
|
}
|
|
23
21
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
async function waitWithCountdown(seconds, label) {
|
|
27
|
-
for (let i = seconds; i > 0; i--) {
|
|
28
|
-
process.stdout.write(`\r ${label} ${i}s… `);
|
|
29
|
-
await sleep(1000);
|
|
30
|
-
}
|
|
31
|
-
process.stdout.write(`\r ${label} pronto! \n`);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
22
|
function openUrl(url) {
|
|
35
23
|
try {
|
|
36
24
|
const { exec } = require('node:child_process');
|
|
@@ -716,7 +704,7 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
716
704
|
const selectedOrgId = await promptOrganizationIfNeeded(tr, cancel);
|
|
717
705
|
let selectedBillingId = await promptBillingAccountIfNeeded(tr, cancel);
|
|
718
706
|
ui.log.info(`${tr('new.firebase.create.estimatedTime')}\n${tr('new.internet.warning')}`);
|
|
719
|
-
const ps1 = ui.
|
|
707
|
+
const ps1 = ui.makeTimedStepper();
|
|
720
708
|
ps1.next(tr('new.firebase.create.creating'));
|
|
721
709
|
const setupResult = await setupFromScratch(core.appName.trim(), core.bundleId.trim(), {
|
|
722
710
|
includeWeb: firebaseIncludeWeb,
|
|
@@ -796,7 +784,7 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
796
784
|
}
|
|
797
785
|
ui.log.message(tr('new.firebase.create.billingRetry.retrying'));
|
|
798
786
|
selectedBillingId = await promptBillingAccountIfNeeded(tr, cancel);
|
|
799
|
-
const ps2 = ui.
|
|
787
|
+
const ps2 = ui.makeTimedStepper();
|
|
800
788
|
ps2.next(tr('new.firebase.create.creating'));
|
|
801
789
|
lastResult = await setupFromScratch(core.appName.trim(), core.bundleId.trim(), {
|
|
802
790
|
includeWeb: firebaseIncludeWeb,
|
|
@@ -889,7 +877,7 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
889
877
|
const selectedOrgId = await promptOrganizationIfNeeded(tr, cancel);
|
|
890
878
|
const selectedBillingId = await promptBillingAccountIfNeeded(tr, cancel);
|
|
891
879
|
ui.log.info(tr('new.internet.warning'));
|
|
892
|
-
const ps3 = ui.
|
|
880
|
+
const ps3 = ui.makeTimedStepper();
|
|
893
881
|
ps3.next(tr('new.firebase.create.creatingPush'));
|
|
894
882
|
const setupResult = await setupFromScratch(core.appName.trim(), core.bundleId.trim(), {
|
|
895
883
|
includeWeb: true,
|
|
@@ -990,7 +978,7 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
990
978
|
});
|
|
991
979
|
supabaseDbPassword = dbPassword;
|
|
992
980
|
ui.log.info(tr('new.internet.warning'));
|
|
993
|
-
const createSpinner = ui.
|
|
981
|
+
const createSpinner = ui.timedSpinner();
|
|
994
982
|
createSpinner.start(tr('new.supabase.creating'));
|
|
995
983
|
supabaseCreateResult = await createProjectAndGetKeys(
|
|
996
984
|
core.appName.trim().replace(/\s+/g, '-').toLowerCase(),
|
|
@@ -1087,7 +1075,7 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1087
1075
|
|
|
1088
1076
|
// ── Firebase existing project: enable APIs + create Firestore/Storage ───
|
|
1089
1077
|
if (backend === 'firebase' && firebaseSetupMode === 'existing' && core.firebaseProjectId) {
|
|
1090
|
-
const ps4 = ui.
|
|
1078
|
+
const ps4 = ui.makeTimedStepper();
|
|
1091
1079
|
ps4.next(stepProgress('enable-apis', language));
|
|
1092
1080
|
const existingSetup = await setupExistingProject(core.firebaseProjectId, {
|
|
1093
1081
|
onProgress: (key, data) => {
|
|
@@ -1388,7 +1376,7 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1388
1376
|
// Stepper shows each step closing as ✦ and the next starting as ⠙ — so the
|
|
1389
1377
|
// user gets explicit "X done → Y starting" feedback instead of a single
|
|
1390
1378
|
// spinner with a mutating message.
|
|
1391
|
-
const stepper = ui.
|
|
1379
|
+
const stepper = ui.makeTimedStepper();
|
|
1392
1380
|
// First step started here so even silent prep work shows progress.
|
|
1393
1381
|
stepper.next(stepProgress('project-setup', language));
|
|
1394
1382
|
|
|
@@ -1512,7 +1500,7 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1512
1500
|
// ── FCM Service Account key (best effort via gcloud — Firebase uses ADC, Supabase needs JSON) ──
|
|
1513
1501
|
let fcmServiceAccountJson = null;
|
|
1514
1502
|
if (answers.firebaseProjectId) {
|
|
1515
|
-
const fcmSpinner = ui.
|
|
1503
|
+
const fcmSpinner = ui.timedSpinner();
|
|
1516
1504
|
fcmSpinner.start(tr('new.fcm.generating'));
|
|
1517
1505
|
const fcmResult = await createFcmServiceAccountKey(answers.firebaseProjectId);
|
|
1518
1506
|
fcmSpinner.stop(tr('new.fcm.generating'));
|
|
@@ -1562,7 +1550,7 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1562
1550
|
|
|
1563
1551
|
// ── API: FCM Service Account key — save to .kasy/ for server configuration ──
|
|
1564
1552
|
if (backend === 'api' && answers.firebaseProjectId) {
|
|
1565
|
-
const fcmSpinner = ui.
|
|
1553
|
+
const fcmSpinner = ui.timedSpinner();
|
|
1566
1554
|
fcmSpinner.start(tr('new.fcm.generating'));
|
|
1567
1555
|
const fcmResult = await createFcmServiceAccountKey(answers.firebaseProjectId);
|
|
1568
1556
|
fcmSpinner.stop(tr('new.fcm.generating'));
|
package/lib/commands/remove.js
CHANGED
|
@@ -444,7 +444,7 @@ async function runRemove(module, options = {}) {
|
|
|
444
444
|
'revenuecat', 'analytics', 'sentry', 'onboarding', 'llm_chat', 'feedback',
|
|
445
445
|
].includes(normalized);
|
|
446
446
|
if (needsBuildRunner) {
|
|
447
|
-
const spinner = ui.
|
|
447
|
+
const spinner = ui.timedSpinner();
|
|
448
448
|
spinner.start(t('remove.buildRunner'));
|
|
449
449
|
try {
|
|
450
450
|
await execAsync(
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
const path = require('node:path');
|
|
2
|
+
const { spawnSync } = require('node:child_process');
|
|
3
|
+
const fs = require('fs-extra');
|
|
4
|
+
const kleur = require('kleur');
|
|
5
|
+
const ui = require('../utils/ui');
|
|
6
|
+
const { createTranslator, detectDefaultLanguage } = require('../utils/i18n');
|
|
7
|
+
const { printCompactHeader } = require('../utils/brand');
|
|
8
|
+
const { readBundleId, readPackageName } = require('../utils/mobile-identity');
|
|
9
|
+
const { spawnFlutterWithSpinner } = require('../utils/flutter-run');
|
|
10
|
+
|
|
11
|
+
function runCmd(cmd, args) {
|
|
12
|
+
const res = spawnSync(cmd, args, { encoding: 'utf8' });
|
|
13
|
+
return {
|
|
14
|
+
code: res.status,
|
|
15
|
+
stdout: (res.stdout || '').trim(),
|
|
16
|
+
stderr: (res.stderr || '').trim(),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function listFlutterDevices(projectDir) {
|
|
21
|
+
const res = spawnSync('flutter', ['devices', '--machine'], {
|
|
22
|
+
cwd: projectDir,
|
|
23
|
+
encoding: 'utf8',
|
|
24
|
+
});
|
|
25
|
+
if (res.status !== 0) return [];
|
|
26
|
+
try {
|
|
27
|
+
const parsed = JSON.parse(res.stdout);
|
|
28
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
29
|
+
} catch {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function classifyTarget(device) {
|
|
35
|
+
const platform = (device.targetPlatform || '').toLowerCase();
|
|
36
|
+
if (platform === 'ios') {
|
|
37
|
+
return device.emulator ? 'ios-simulator' : 'ios-device';
|
|
38
|
+
}
|
|
39
|
+
if (platform.startsWith('android')) {
|
|
40
|
+
return device.emulator ? 'android-emulator' : 'android-device';
|
|
41
|
+
}
|
|
42
|
+
if (platform.startsWith('web')) {
|
|
43
|
+
return 'web';
|
|
44
|
+
}
|
|
45
|
+
return 'unknown';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function filterDevices(devices, options) {
|
|
49
|
+
return devices.filter((d) => {
|
|
50
|
+
const kind = classifyTarget(d);
|
|
51
|
+
if (options.device && d.id === options.device) return true;
|
|
52
|
+
if (options.ios && (kind === 'ios-simulator' || kind === 'ios-device')) {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
if (
|
|
56
|
+
options.android &&
|
|
57
|
+
(kind === 'android-emulator' || kind === 'android-device')
|
|
58
|
+
) {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
if (options.web && kind === 'web') return true;
|
|
62
|
+
if (!options.ios && !options.android && !options.web && !options.device) {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
return false;
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function pickDevice(devices, t) {
|
|
70
|
+
if (devices.length === 0) return null;
|
|
71
|
+
if (devices.length === 1) return devices[0];
|
|
72
|
+
const choice = await ui.select({
|
|
73
|
+
message: t('reset.prompt.pickDevice'),
|
|
74
|
+
options: devices.map((d) => ({
|
|
75
|
+
value: d.id,
|
|
76
|
+
label: `${d.name} ${kleur.dim(`(${classifyTarget(d)})`)}`,
|
|
77
|
+
})),
|
|
78
|
+
});
|
|
79
|
+
return devices.find((d) => d.id === choice) || null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function resetIosSimulator(device, bundleId, t) {
|
|
83
|
+
ui.log.message(kleur.dim(`xcrun simctl uninstall ${device.id} ${bundleId}`));
|
|
84
|
+
const res = runCmd('xcrun', ['simctl', 'uninstall', device.id, bundleId]);
|
|
85
|
+
if (res.code !== 0) {
|
|
86
|
+
// simctl returns 0 even when app wasn't installed; non-zero is a real error
|
|
87
|
+
ui.log.warn(res.stderr || t('reset.warn.iosUninstallFailed'));
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
ui.log.success(t('reset.success.uninstalled'));
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function resetIosDevice(device, bundleId, t) {
|
|
95
|
+
// Try Apple's devicectl (ships with Xcode 15+). Falls back to a manual
|
|
96
|
+
// instruction when devicectl isn't available or the uninstall fails.
|
|
97
|
+
const probe = runCmd('xcrun', ['devicectl', '--version']);
|
|
98
|
+
if (probe.code !== 0) {
|
|
99
|
+
noticeIosPhysical(t);
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
ui.log.message(
|
|
103
|
+
kleur.dim(
|
|
104
|
+
`xcrun devicectl device uninstall app --device ${device.id} ${bundleId}`
|
|
105
|
+
)
|
|
106
|
+
);
|
|
107
|
+
const res = runCmd('xcrun', [
|
|
108
|
+
'devicectl',
|
|
109
|
+
'device',
|
|
110
|
+
'uninstall',
|
|
111
|
+
'app',
|
|
112
|
+
'--device',
|
|
113
|
+
device.id,
|
|
114
|
+
bundleId,
|
|
115
|
+
]);
|
|
116
|
+
if (res.code !== 0) {
|
|
117
|
+
ui.log.warn(res.stderr || t('reset.warn.iosDeviceUninstallFailed'));
|
|
118
|
+
noticeIosPhysical(t);
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
ui.log.success(t('reset.success.uninstalled'));
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function resetAndroid(device, packageName, t) {
|
|
126
|
+
ui.log.message(kleur.dim(`adb -s ${device.id} uninstall ${packageName}`));
|
|
127
|
+
const res = runCmd('adb', ['-s', device.id, 'uninstall', packageName]);
|
|
128
|
+
// adb prints "Success" or "Failure [DELETE_FAILED_INTERNAL_ERROR]" / "not installed"
|
|
129
|
+
const out = `${res.stdout}\n${res.stderr}`.toLowerCase();
|
|
130
|
+
if (out.includes('success')) {
|
|
131
|
+
ui.log.success(t('reset.success.uninstalled'));
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
if (out.includes('not installed')) {
|
|
135
|
+
ui.log.message(kleur.dim(`– ${t('reset.info.notInstalled')}`));
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
ui.log.warn(res.stdout || res.stderr || t('reset.warn.androidUninstallFailed'));
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function noticeIosPhysical(t) {
|
|
143
|
+
ui.log.warn(t('reset.warn.iosDeviceManual'));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function isXcodeRunning() {
|
|
147
|
+
const res = spawnSync('pgrep', ['-x', 'Xcode'], { encoding: 'utf8' });
|
|
148
|
+
return res.status === 0 && (res.stdout || '').trim().length > 0;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function ensureXcodeClosed(t) {
|
|
152
|
+
if (!isXcodeRunning()) return true;
|
|
153
|
+
const choice = await ui.select({
|
|
154
|
+
message: t('reset.prompt.xcodeOpen'),
|
|
155
|
+
options: [
|
|
156
|
+
{ value: 'close', label: t('reset.prompt.xcodeOpen.close') },
|
|
157
|
+
{ value: 'skip', label: t('reset.prompt.xcodeOpen.skip') },
|
|
158
|
+
{ value: 'cancel', label: t('reset.prompt.xcodeOpen.cancel') },
|
|
159
|
+
],
|
|
160
|
+
initialValue: 'close',
|
|
161
|
+
});
|
|
162
|
+
if (choice === 'cancel') return false;
|
|
163
|
+
if (choice === 'close') {
|
|
164
|
+
spawnSync('osascript', ['-e', 'tell application "Xcode" to quit'], {
|
|
165
|
+
encoding: 'utf8',
|
|
166
|
+
});
|
|
167
|
+
// Give Xcode a moment to release the debug session before we proceed.
|
|
168
|
+
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
169
|
+
ui.log.message(kleur.dim(t('reset.info.xcodeClosed')));
|
|
170
|
+
}
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function noticeWeb(t) {
|
|
175
|
+
ui.log.warn(t('reset.warn.webIncognito'));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function runFlutterOnDevice(device, projectDir, t) {
|
|
179
|
+
return spawnFlutterWithSpinner(['run', '-d', device.id], projectDir, t);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function runReset(directory, options = {}) {
|
|
183
|
+
const t = createTranslator(options.language || detectDefaultLanguage());
|
|
184
|
+
const projectDir = path.resolve(directory || '.');
|
|
185
|
+
|
|
186
|
+
if (!(await fs.pathExists(path.join(projectDir, 'pubspec.yaml')))) {
|
|
187
|
+
throw new Error(t('reset.error.notFlutterProject'));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
printCompactHeader(t);
|
|
191
|
+
ui.intro(t('reset.title'));
|
|
192
|
+
|
|
193
|
+
const [bundleId, packageName] = await Promise.all([
|
|
194
|
+
readBundleId(projectDir),
|
|
195
|
+
readPackageName(projectDir),
|
|
196
|
+
]);
|
|
197
|
+
if (!bundleId && !packageName) {
|
|
198
|
+
throw new Error(t('reset.error.noIdentifier'));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
ui.log.message(`${kleur.dim('iOS bundle id:')} ${bundleId || kleur.dim('—')}`);
|
|
202
|
+
ui.log.message(`${kleur.dim('Android package:')} ${packageName || kleur.dim('—')}`);
|
|
203
|
+
|
|
204
|
+
const scanSpinner = ui.spinner();
|
|
205
|
+
scanSpinner.start(t('reset.scanning'));
|
|
206
|
+
const allDevices = await listFlutterDevices(projectDir);
|
|
207
|
+
const devices = filterDevices(allDevices, options);
|
|
208
|
+
scanSpinner.stop(t('reset.scanning'));
|
|
209
|
+
|
|
210
|
+
if (devices.length === 0) {
|
|
211
|
+
ui.log.warn(t('reset.warn.noDevices'));
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const target = await pickDevice(devices, t);
|
|
216
|
+
if (!target) {
|
|
217
|
+
ui.log.warn(t('reset.warn.nothingSelected'));
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const kind = classifyTarget(target);
|
|
222
|
+
|
|
223
|
+
if (kind === 'ios-device') {
|
|
224
|
+
const proceed = await ensureXcodeClosed(t);
|
|
225
|
+
if (!proceed) {
|
|
226
|
+
ui.outro(t('reset.outro.cancelled'));
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
ui.log.step(kleur.bold(`${t('reset.resetting')}: ${target.name}`));
|
|
232
|
+
|
|
233
|
+
let didReset = false;
|
|
234
|
+
switch (kind) {
|
|
235
|
+
case 'ios-simulator':
|
|
236
|
+
if (!bundleId) {
|
|
237
|
+
ui.log.error(t('reset.error.noBundleId'));
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
didReset = resetIosSimulator(target, bundleId, t);
|
|
241
|
+
break;
|
|
242
|
+
case 'ios-device':
|
|
243
|
+
if (!bundleId) {
|
|
244
|
+
ui.log.error(t('reset.error.noBundleId'));
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
didReset = resetIosDevice(target, bundleId, t);
|
|
248
|
+
break;
|
|
249
|
+
case 'android-emulator':
|
|
250
|
+
case 'android-device':
|
|
251
|
+
if (!packageName) {
|
|
252
|
+
ui.log.error(t('reset.error.noPackageName'));
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
didReset = resetAndroid(target, packageName, t);
|
|
256
|
+
break;
|
|
257
|
+
case 'web':
|
|
258
|
+
noticeWeb(t);
|
|
259
|
+
break;
|
|
260
|
+
default:
|
|
261
|
+
ui.log.warn(`${t('reset.warn.unknownPlatform')}: ${kind}`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (!didReset) {
|
|
265
|
+
ui.outro(t('reset.outro.skipped'));
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (options.reinstall === false) {
|
|
270
|
+
ui.outro(t('reset.outro.uninstalledOnly'));
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const proceed = await ui.confirm({
|
|
275
|
+
message: t('reset.prompt.reinstallNow'),
|
|
276
|
+
initialValue: true,
|
|
277
|
+
});
|
|
278
|
+
if (!proceed) {
|
|
279
|
+
ui.outro(t('reset.outro.uninstalledOnly'));
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
ui.log.step(kleur.bold(t('reset.reinstalling')));
|
|
284
|
+
await runFlutterOnDevice(target, projectDir, t);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
module.exports = { runReset };
|
package/lib/commands/run.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
const { spawn } = require('node:child_process');
|
|
2
1
|
const path = require('node:path');
|
|
3
2
|
const fs = require('fs-extra');
|
|
4
3
|
const kleur = require('kleur');
|
|
5
4
|
const { createTranslator, detectDefaultLanguage } = require('../utils/i18n');
|
|
6
5
|
const { printCompactHeader } = require('../utils/brand');
|
|
6
|
+
const { spawnFlutterWithSpinner } = require('../utils/flutter-run');
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Read dart-define args from .vscode/launch.json.
|
|
@@ -54,25 +54,32 @@ async function runRun(directory, options = {}) {
|
|
|
54
54
|
|
|
55
55
|
const args = ['run', ...deviceArgs, ...dartDefines];
|
|
56
56
|
|
|
57
|
+
const envDefine = dartDefines.find((a) => a.startsWith('--dart-define=ENV='));
|
|
58
|
+
const envValue = envDefine ? envDefine.split('=').pop() : null;
|
|
59
|
+
const deviceLabel = options.web
|
|
60
|
+
? 'chrome'
|
|
61
|
+
: options.ios
|
|
62
|
+
? 'ios'
|
|
63
|
+
: options.android
|
|
64
|
+
? 'android'
|
|
65
|
+
: options.device || null;
|
|
66
|
+
const summaryParts = [];
|
|
67
|
+
if (envValue) summaryParts.push(`ENV=${envValue}`);
|
|
68
|
+
if (deviceLabel) summaryParts.push(`device: ${deviceLabel}`);
|
|
69
|
+
const summary = summaryParts.length ? ` (${summaryParts.join(', ')})` : '';
|
|
70
|
+
|
|
57
71
|
printCompactHeader(t);
|
|
58
|
-
console.log(kleur.bold(`${t('run.launching')}`));
|
|
59
|
-
console.log(kleur.dim(` flutter ${args.join(' ')}`));
|
|
72
|
+
console.log(kleur.bold(`${t('run.launching')}${summary}`));
|
|
60
73
|
console.log(kleur.dim(` ✦ ${t('run.updateHint.prefix')} `) + kleur.cyan('kasy update') + kleur.dim(` ${t('run.updateHint.suffix')}\n`));
|
|
61
74
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
reject(new Error(t('run.error.flutterNotFound')));
|
|
71
|
-
} else {
|
|
72
|
-
reject(err);
|
|
73
|
-
}
|
|
74
|
-
});
|
|
75
|
-
});
|
|
75
|
+
try {
|
|
76
|
+
await spawnFlutterWithSpinner(args, projectDir, t);
|
|
77
|
+
} catch (err) {
|
|
78
|
+
if (err.code === 'ENOENT') {
|
|
79
|
+
throw new Error(t('run.error.flutterNotFound'));
|
|
80
|
+
}
|
|
81
|
+
throw err;
|
|
82
|
+
}
|
|
76
83
|
}
|
|
77
84
|
|
|
78
85
|
module.exports = { runRun };
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
const path = require('node:path');
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
const fsp = require('node:fs/promises');
|
|
4
|
+
const { exec } = require('node:child_process');
|
|
5
|
+
const { promisify } = require('node:util');
|
|
6
|
+
const kleur = require('kleur');
|
|
7
|
+
const ui = require('../utils/ui');
|
|
8
|
+
const { printCompactHeader } = require('../utils/brand');
|
|
9
|
+
const { createTranslator, detectDefaultLanguage } = require('../utils/i18n');
|
|
10
|
+
|
|
11
|
+
const execAsync = promisify(exec);
|
|
12
|
+
|
|
13
|
+
const ASSETS_DIR = path.join('assets', 'images');
|
|
14
|
+
const LIGHT_NAME = 'splash_logo_light.png';
|
|
15
|
+
const DARK_NAME = 'splash_logo_dark.png';
|
|
16
|
+
|
|
17
|
+
const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Parse the PNG IHDR chunk to read color type and detect transparency.
|
|
21
|
+
*
|
|
22
|
+
* PNG layout: 8-byte signature + chunks. The IHDR chunk is always first,
|
|
23
|
+
* placed at byte offset 8. It holds: length(4) + "IHDR"(4) + data(13) + crc(4).
|
|
24
|
+
* Color type is the 10th byte of data → absolute offset 25.
|
|
25
|
+
*
|
|
26
|
+
* Color types with alpha or transparency support:
|
|
27
|
+
* 4 = grayscale + alpha
|
|
28
|
+
* 6 = truecolor + alpha
|
|
29
|
+
* 3 = indexed (transparency only if a tRNS chunk is present)
|
|
30
|
+
*
|
|
31
|
+
* @param {string} filePath
|
|
32
|
+
* @returns {Promise<{ valid: boolean, hasAlpha: boolean, colorType: number, width: number, height: number }>}
|
|
33
|
+
*/
|
|
34
|
+
async function inspectPng(filePath) {
|
|
35
|
+
const handle = await fsp.open(filePath, 'r');
|
|
36
|
+
try {
|
|
37
|
+
const header = Buffer.alloc(33);
|
|
38
|
+
await handle.read(header, 0, 33, 0);
|
|
39
|
+
const sig = header.subarray(0, 8);
|
|
40
|
+
if (!sig.equals(PNG_SIGNATURE)) {
|
|
41
|
+
return { valid: false, hasAlpha: false, colorType: -1, width: 0, height: 0 };
|
|
42
|
+
}
|
|
43
|
+
const width = header.readUInt32BE(16);
|
|
44
|
+
const height = header.readUInt32BE(20);
|
|
45
|
+
const colorType = header.readUInt8(25);
|
|
46
|
+
|
|
47
|
+
let hasAlpha = colorType === 4 || colorType === 6;
|
|
48
|
+
|
|
49
|
+
if (!hasAlpha && colorType === 3) {
|
|
50
|
+
const { size } = await handle.stat();
|
|
51
|
+
const remaining = Math.min(size - 33, 256 * 1024);
|
|
52
|
+
if (remaining > 0) {
|
|
53
|
+
const tail = Buffer.alloc(remaining);
|
|
54
|
+
await handle.read(tail, 0, remaining, 33);
|
|
55
|
+
if (tail.includes(Buffer.from('tRNS'))) {
|
|
56
|
+
hasAlpha = true;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return { valid: true, hasAlpha, colorType, width, height };
|
|
62
|
+
} finally {
|
|
63
|
+
await handle.close();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function assertKasyProject(projectDir, t) {
|
|
68
|
+
const kitSetupPath = path.join(projectDir, 'kit_setup.json');
|
|
69
|
+
const pubspecPath = path.join(projectDir, 'pubspec.yaml');
|
|
70
|
+
if (!(await fs.pathExists(kitSetupPath)) && !(await fs.pathExists(pubspecPath))) {
|
|
71
|
+
throw new Error(t('splash.error.notKasyProject'));
|
|
72
|
+
}
|
|
73
|
+
const assetsDir = path.join(projectDir, ASSETS_DIR);
|
|
74
|
+
await fs.ensureDir(assetsDir);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @param {string} flagValue
|
|
79
|
+
*/
|
|
80
|
+
function resolveInputPath(flagValue) {
|
|
81
|
+
if (!flagValue) return null;
|
|
82
|
+
const expanded = flagValue.startsWith('~')
|
|
83
|
+
? path.join(require('node:os').homedir(), flagValue.slice(1))
|
|
84
|
+
: flagValue;
|
|
85
|
+
return path.resolve(expanded);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @param {string} projectDir
|
|
90
|
+
* @param {{ light?: string, dark?: string, skipGenerate?: boolean, language?: string }} options
|
|
91
|
+
*/
|
|
92
|
+
async function runSplash(projectDir, options = {}) {
|
|
93
|
+
const language = options.language || detectDefaultLanguage();
|
|
94
|
+
const t = createTranslator(language);
|
|
95
|
+
|
|
96
|
+
printCompactHeader();
|
|
97
|
+
ui.intro(kleur.bold().cyan(t('splash.intro')));
|
|
98
|
+
|
|
99
|
+
await assertKasyProject(projectDir, t);
|
|
100
|
+
|
|
101
|
+
const lightPath = resolveInputPath(options.light);
|
|
102
|
+
const darkPath = resolveInputPath(options.dark);
|
|
103
|
+
|
|
104
|
+
if (!lightPath || !darkPath) {
|
|
105
|
+
ui.log.error(t('splash.error.bothRequired'));
|
|
106
|
+
ui.log.message(kleur.dim('kasy splash --light <light.png> --dark <dark.png>'));
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
for (const [role, p] of [['light', lightPath], ['dark', darkPath]]) {
|
|
111
|
+
if (!(await fs.pathExists(p))) {
|
|
112
|
+
ui.log.error(t('splash.error.fileNotFound', { path: p }));
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const inspectSpinner = ui.spinner();
|
|
118
|
+
inspectSpinner.start(t('splash.validating'));
|
|
119
|
+
|
|
120
|
+
let lightInfo;
|
|
121
|
+
let darkInfo;
|
|
122
|
+
try {
|
|
123
|
+
lightInfo = await inspectPng(lightPath);
|
|
124
|
+
darkInfo = await inspectPng(darkPath);
|
|
125
|
+
} catch (err) {
|
|
126
|
+
inspectSpinner.stop(`✖ ${err.message || t('splash.error.notPng')}`);
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (!lightInfo.valid || !darkInfo.valid) {
|
|
131
|
+
inspectSpinner.stop(`✖ ${t('splash.error.notPng')}`);
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
inspectSpinner.stop(t('splash.validated'));
|
|
136
|
+
|
|
137
|
+
const warnings = [];
|
|
138
|
+
if (!lightInfo.hasAlpha) {
|
|
139
|
+
warnings.push(t('splash.warn.noAlphaLight', { path: path.basename(lightPath) }));
|
|
140
|
+
}
|
|
141
|
+
if (!darkInfo.hasAlpha) {
|
|
142
|
+
warnings.push(t('splash.warn.noAlphaDark', { path: path.basename(darkPath) }));
|
|
143
|
+
}
|
|
144
|
+
if (lightInfo.width < 768 || lightInfo.height < 768) {
|
|
145
|
+
warnings.push(t('splash.warn.smallLight', {
|
|
146
|
+
w: lightInfo.width,
|
|
147
|
+
h: lightInfo.height,
|
|
148
|
+
}));
|
|
149
|
+
}
|
|
150
|
+
if (darkInfo.width < 768 || darkInfo.height < 768) {
|
|
151
|
+
warnings.push(t('splash.warn.smallDark', {
|
|
152
|
+
w: darkInfo.width,
|
|
153
|
+
h: darkInfo.height,
|
|
154
|
+
}));
|
|
155
|
+
}
|
|
156
|
+
if (warnings.length > 0) {
|
|
157
|
+
ui.note(warnings.map((w) => `${kleur.yellow('⚠')} ${w}`).join('\n'), t('splash.warn.title'));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const destLight = path.join(projectDir, ASSETS_DIR, LIGHT_NAME);
|
|
161
|
+
const destDark = path.join(projectDir, ASSETS_DIR, DARK_NAME);
|
|
162
|
+
|
|
163
|
+
const copySpinner = ui.spinner();
|
|
164
|
+
copySpinner.start(t('splash.copying'));
|
|
165
|
+
await fs.copy(lightPath, destLight, { overwrite: true });
|
|
166
|
+
await fs.copy(darkPath, destDark, { overwrite: true });
|
|
167
|
+
copySpinner.stop(t('splash.copied'));
|
|
168
|
+
|
|
169
|
+
if (options.skipGenerate) {
|
|
170
|
+
ui.note(t('splash.skipGenerate.hint'), t('splash.skipGenerate.title'));
|
|
171
|
+
ui.outro(t('splash.done'));
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const genSpinner = ui.spinner();
|
|
176
|
+
genSpinner.start(t('splash.generating'));
|
|
177
|
+
const result = await runFlutterNativeSplash(projectDir);
|
|
178
|
+
if (result.ok) {
|
|
179
|
+
genSpinner.stop(t('splash.generated'));
|
|
180
|
+
} else {
|
|
181
|
+
genSpinner.stop(`⚠ ${t('splash.error.generateFailed')}`);
|
|
182
|
+
if (result.stderr) {
|
|
183
|
+
ui.log.message(kleur.dim(result.stderr.split('\n').slice(0, 8).join('\n')));
|
|
184
|
+
}
|
|
185
|
+
ui.log.message(kleur.dim('dart run flutter_native_splash:create'));
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const summary = [
|
|
190
|
+
`${kleur.bold(t('splash.summary.light'))}: ${kleur.white(LIGHT_NAME)} (${lightInfo.width}x${lightInfo.height})`,
|
|
191
|
+
`${kleur.bold(t('splash.summary.dark'))}: ${kleur.white(DARK_NAME)} (${darkInfo.width}x${darkInfo.height})`,
|
|
192
|
+
].join('\n');
|
|
193
|
+
ui.note(summary, t('splash.summary.title'));
|
|
194
|
+
|
|
195
|
+
ui.outro(t('splash.done'));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function runFlutterNativeSplash(projectDir) {
|
|
199
|
+
try {
|
|
200
|
+
const { stdout, stderr } = await execAsync(
|
|
201
|
+
'dart run flutter_native_splash:create',
|
|
202
|
+
{
|
|
203
|
+
cwd: projectDir,
|
|
204
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
205
|
+
timeout: 240_000,
|
|
206
|
+
},
|
|
207
|
+
);
|
|
208
|
+
return { ok: true, stdout, stderr };
|
|
209
|
+
} catch (err) {
|
|
210
|
+
return {
|
|
211
|
+
ok: false,
|
|
212
|
+
error: err.message,
|
|
213
|
+
stdout: err.stdout || '',
|
|
214
|
+
stderr: err.stderr || '',
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
module.exports = { runSplash, inspectPng };
|
package/lib/commands/update.js
CHANGED
|
@@ -374,7 +374,7 @@ async function runUpdate(module, options = {}) {
|
|
|
374
374
|
|
|
375
375
|
// build_runner (only for modules that generate code)
|
|
376
376
|
if (NEEDS_BUILD_RUNNER.includes(normalized)) {
|
|
377
|
-
const spinner = ui.
|
|
377
|
+
const spinner = ui.timedSpinner();
|
|
378
378
|
spinner.start(t('update.buildRunner'));
|
|
379
379
|
try {
|
|
380
380
|
await execAsync(
|
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
{
|
|
2
|
+
"1.13.0": {
|
|
3
|
+
"modules": {
|
|
4
|
+
"onboarding": {
|
|
5
|
+
"pt": "Botão \"Já tem conta? Entrar\" na primeira tela do onboarding — quem já tem conta entra direto sem passar pelo fluxo todo, sem precisar criar usuário anônimo antes",
|
|
6
|
+
"en": "\"Already have an account? Log in\" button on the first onboarding screen — returning users go straight to sign-in instead of going through the full flow as an anonymous user first",
|
|
7
|
+
"es": "Botón \"¿Ya tienes cuenta? Iniciar sesión\" en la primera pantalla del onboarding — quien ya tiene cuenta entra directo sin pasar por todo el flujo como usuario anónimo"
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
},
|
|
2
11
|
"1.10.0": {
|
|
3
12
|
"modules": {
|
|
4
13
|
"widget": {
|