kasy-cli 1.13.0 → 1.15.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.
Files changed (157) hide show
  1. package/bin/kasy.js +140 -12
  2. package/lib/commands/add.js +2 -2
  3. package/lib/commands/codemagic.js +11 -4
  4. package/lib/commands/deploy.js +3 -3
  5. package/lib/commands/favicon.js +115 -0
  6. package/lib/commands/icon.js +143 -0
  7. package/lib/commands/ios.js +28 -7
  8. package/lib/commands/new.js +8 -20
  9. package/lib/commands/remove.js +1 -1
  10. package/lib/commands/reset.js +385 -0
  11. package/lib/commands/run.js +24 -17
  12. package/lib/commands/splash.js +14 -4
  13. package/lib/commands/update.js +1 -1
  14. package/lib/scaffold/backends/api/patch/README.md +1 -1
  15. package/lib/scaffold/backends/api/patch/android/app/src/main/AndroidManifest.xml +1 -1
  16. package/lib/scaffold/backends/api/patch/lib/features/notifications/api/device_api.dart +53 -0
  17. package/lib/scaffold/backends/api/pubspec.yaml.tpl +11 -1
  18. package/lib/scaffold/backends/firebase/tokens.js +2 -2
  19. package/lib/scaffold/backends/supabase/edge-functions/send-push-notification/index.ts +8 -2
  20. package/lib/scaffold/backends/supabase/migrations/20240101000011_dedupe_device_tokens.sql +34 -0
  21. package/lib/scaffold/backends/supabase/patch/README.md +1 -1
  22. package/lib/scaffold/backends/supabase/patch/android/app/src/main/AndroidManifest.xml +1 -1
  23. package/lib/scaffold/backends/supabase/patch/lib/features/notifications/api/device_api.dart +43 -0
  24. package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +11 -1
  25. package/lib/utils/apple-release.js +115 -16
  26. package/lib/utils/checks.js +45 -107
  27. package/lib/utils/debug.js +75 -0
  28. package/lib/utils/flutter-run.js +173 -0
  29. package/lib/utils/friendly-error.js +91 -0
  30. package/lib/utils/i18n/messages-en.js +970 -0
  31. package/lib/utils/i18n/messages-es.js +968 -0
  32. package/lib/utils/i18n/messages-pt.js +968 -0
  33. package/lib/utils/i18n.js +21 -2483
  34. package/lib/utils/mobile-identity.js +35 -0
  35. package/lib/utils/png-padding.js +120 -0
  36. package/lib/utils/ui.js +114 -0
  37. package/package.json +8 -4
  38. package/templates/firebase/README.en.md +1 -1
  39. package/templates/firebase/README.es.md +1 -1
  40. package/templates/firebase/README.md +1 -1
  41. package/templates/firebase/android/app/build.gradle.kts +10 -1
  42. package/templates/firebase/android/app/src/main/AndroidManifest.xml +1 -1
  43. package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/MainActivity.kt +25 -1
  44. package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/MyWidget.kt +161 -11
  45. package/templates/firebase/android/app/src/main/res/drawable/widget_add_button.xml +15 -0
  46. package/templates/firebase/android/app/src/main/res/drawable/widget_gradient_bg.xml +9 -0
  47. package/templates/firebase/android/app/src/main/res/drawable/widget_gradient_inner.xml +12 -0
  48. package/templates/firebase/android/app/src/main/res/drawable/widget_plan_pill_bg.xml +5 -0
  49. package/templates/firebase/android/app/src/main/res/drawable/widget_preview_image.xml +17 -0
  50. package/templates/firebase/android/app/src/main/res/drawable/widget_pro_pill_bg.xml +5 -0
  51. package/templates/firebase/android/app/src/main/res/drawable-hdpi/android12splash.png +0 -0
  52. package/templates/firebase/android/app/src/main/res/drawable-hdpi/ic_launcher_background.png +0 -0
  53. package/templates/firebase/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png +0 -0
  54. package/templates/firebase/android/app/src/main/res/drawable-mdpi/android12splash.png +0 -0
  55. package/templates/firebase/android/app/src/main/res/drawable-mdpi/ic_launcher_background.png +0 -0
  56. package/templates/firebase/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png +0 -0
  57. package/templates/firebase/android/app/src/main/res/drawable-night-hdpi/android12splash.png +0 -0
  58. package/templates/firebase/android/app/src/main/res/drawable-night-mdpi/android12splash.png +0 -0
  59. package/templates/firebase/android/app/src/main/res/drawable-night-xhdpi/android12splash.png +0 -0
  60. package/templates/firebase/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png +0 -0
  61. package/templates/firebase/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png +0 -0
  62. package/templates/firebase/android/app/src/main/res/drawable-xhdpi/android12splash.png +0 -0
  63. package/templates/firebase/android/app/src/main/res/drawable-xhdpi/ic_launcher_background.png +0 -0
  64. package/templates/firebase/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png +0 -0
  65. package/templates/firebase/android/app/src/main/res/drawable-xxhdpi/android12splash.png +0 -0
  66. package/templates/firebase/android/app/src/main/res/drawable-xxhdpi/ic_launcher_background.png +0 -0
  67. package/templates/firebase/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png +0 -0
  68. package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/android12splash.png +0 -0
  69. package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_background.png +0 -0
  70. package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png +0 -0
  71. package/templates/firebase/android/app/src/main/res/layout/widget_loading.xml +8 -0
  72. package/templates/firebase/android/app/src/main/res/layout/widget_preview.xml +53 -0
  73. package/templates/firebase/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +9 -0
  74. package/templates/firebase/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
  75. package/templates/firebase/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
  76. package/templates/firebase/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
  77. package/templates/firebase/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  78. package/templates/firebase/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  79. package/templates/firebase/android/app/src/main/res/xml/mywidget_info.xml +9 -3
  80. package/templates/firebase/assets/images/favicon.png +0 -0
  81. package/templates/firebase/assets/images/icon.png +0 -0
  82. package/templates/firebase/assets/images/icon_android.png +0 -0
  83. package/templates/firebase/assets/images/icon_foreground_empty.png +0 -0
  84. package/templates/firebase/assets/images/splash_logo_dark_android12.png +0 -0
  85. package/templates/firebase/assets/images/splash_logo_light_android12.png +0 -0
  86. package/templates/firebase/firestore.indexes.json +10 -0
  87. package/templates/firebase/functions/src/core/data/entities/user_device_entity.ts +3 -0
  88. package/templates/firebase/functions/src/core/data/repositories/user_device_repository.ts +17 -1
  89. package/templates/firebase/functions/src/index.ts +1 -0
  90. package/templates/firebase/functions/src/notifications/device_triggers.ts +58 -0
  91. package/templates/firebase/ios/HomeWidgetExtension/MyWidget.swift +116 -33
  92. package/templates/firebase/ios/Runner/AppDelegate.swift +17 -1
  93. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png +0 -0
  94. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png +0 -0
  95. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png +0 -0
  96. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png +0 -0
  97. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png +0 -0
  98. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png +0 -0
  99. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png +0 -0
  100. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png +0 -0
  101. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png +0 -0
  102. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png +0 -0
  103. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png +0 -0
  104. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png +0 -0
  105. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png +0 -0
  106. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png +0 -0
  107. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png +0 -0
  108. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png +0 -0
  109. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png +0 -0
  110. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png +0 -0
  111. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png +0 -0
  112. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png +0 -0
  113. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png +0 -0
  114. package/templates/firebase/ios/Runner/Info.plist +2 -2
  115. package/templates/firebase/ios/Runner/es.lproj/InfoPlist.strings +1 -1
  116. package/templates/firebase/ios/Runner/pt-BR.lproj/InfoPlist.strings +1 -1
  117. package/templates/firebase/ios/Runner/pt.lproj/InfoPlist.strings +1 -1
  118. package/templates/firebase/lib/components/components.dart +1 -0
  119. package/templates/firebase/lib/components/kasy_avatar.dart +88 -57
  120. package/templates/firebase/lib/components/kasy_avatar_presets.dart +116 -74
  121. package/templates/firebase/lib/components/kasy_button.dart +8 -0
  122. package/templates/firebase/lib/components/kasy_tabs.dart +431 -0
  123. package/templates/firebase/lib/core/bottom_menu/kasy_bottom_bar_factory.dart +18 -0
  124. package/templates/firebase/lib/core/home_widgets/home_widget_mywidget_service.dart +73 -19
  125. package/templates/firebase/lib/core/home_widgets/home_widget_service.dart +22 -8
  126. package/templates/firebase/lib/core/shared_preferences/shared_preferences.dart +16 -4
  127. package/templates/firebase/lib/core/states/components/maybeshow_component.dart +4 -8
  128. package/templates/firebase/lib/core/states/user_state_notifier.dart +13 -1
  129. package/templates/firebase/lib/features/home/home_components_page.dart +1 -1
  130. package/templates/firebase/lib/features/home/home_components_preview_registry.dart +316 -93
  131. package/templates/firebase/lib/features/home/home_page.dart +0 -6
  132. package/templates/firebase/lib/features/notifications/api/device_api.dart +57 -0
  133. package/templates/firebase/lib/features/notifications/providers/models/notification.dart +11 -1
  134. package/templates/firebase/lib/features/notifications/repositories/device_repository.dart +9 -0
  135. package/templates/firebase/lib/features/notifications/repositories/notifications_repository.dart +1 -4
  136. package/templates/firebase/lib/features/notifications/shared/att_permission.dart +28 -8
  137. package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +16 -1
  138. package/templates/firebase/lib/features/notifications/ui/widgets/empty_notifications.dart +44 -11
  139. package/templates/firebase/lib/features/settings/ui/components/admin/admin_bottom_sheet.dart +31 -29
  140. package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +21 -5
  141. package/templates/firebase/lib/i18n/en.i18n.json +4 -1
  142. package/templates/firebase/lib/i18n/es.i18n.json +4 -1
  143. package/templates/firebase/lib/i18n/pt.i18n.json +4 -1
  144. package/templates/firebase/pubspec.yaml +10 -3
  145. package/templates/firebase/test/features/notifications/data/device_api_fake.dart +9 -0
  146. package/templates/firebase/web/favicon.png +0 -0
  147. package/templates/firebase/web/icons/Icon-192.png +0 -0
  148. package/templates/firebase/web/icons/Icon-512.png +0 -0
  149. package/templates/firebase/web/icons/Icon-maskable-192.png +0 -0
  150. package/templates/firebase/web/icons/Icon-maskable-512.png +0 -0
  151. package/templates/firebase/web/index.html +9 -0
  152. package/templates/firebase/web/manifest.json +3 -3
  153. package/templates/firebase/assets/images/app_icon.png +0 -0
  154. package/templates/firebase/assets/images/onboarding/img1.jpg +0 -0
  155. package/templates/firebase/assets/images/onboarding/onboarding.png +0 -0
  156. package/templates/firebase/lib/core/states/components/maybe_ask_biometric_setup.dart +0 -88
  157. package/templates/firebase/lib/features/notifications/shared/notification_permission_bottom_sheet.dart +0 -144
@@ -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.makeStepper();
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.makeStepper();
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.makeStepper();
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.spinner();
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.makeStepper();
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.makeStepper();
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.spinner();
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.spinner();
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'));
@@ -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.spinner();
447
+ const spinner = ui.timedSpinner();
448
448
  spinner.start(t('remove.buildRunner'));
449
449
  try {
450
450
  await execAsync(
@@ -0,0 +1,385 @@
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
+ // Force-stop first — `adb uninstall` returns DELETE_FAILED_INTERNAL_ERROR
127
+ // when the app has lingering processes or pending PM state (common on
128
+ // Pixel emulators after a crash or hot-restart).
129
+ runCmd('adb', ['-s', device.id, 'shell', 'am', 'force-stop', packageName]);
130
+
131
+ ui.log.message(kleur.dim(`adb -s ${device.id} uninstall ${packageName}`));
132
+ const res = runCmd('adb', ['-s', device.id, 'uninstall', packageName]);
133
+ const out = `${res.stdout}\n${res.stderr}`.toLowerCase();
134
+ if (out.includes('success')) {
135
+ ui.log.success(t('reset.success.uninstalled'));
136
+ return true;
137
+ }
138
+ if (out.includes('not installed')) {
139
+ ui.log.message(kleur.dim(`– ${t('reset.info.notInstalled')}`));
140
+ return true;
141
+ }
142
+
143
+ // Fallback: `pm uninstall --user 0` bypasses several PM states that
144
+ // `adb uninstall` can't (DELETE_FAILED_INTERNAL_ERROR, multi-user, etc).
145
+ ui.log.message(
146
+ kleur.dim(`adb -s ${device.id} shell pm uninstall --user 0 ${packageName}`)
147
+ );
148
+ const fallback = runCmd('adb', [
149
+ '-s', device.id, 'shell', 'pm', 'uninstall', '--user', '0', packageName,
150
+ ]);
151
+ const fallbackOut = `${fallback.stdout}\n${fallback.stderr}`.toLowerCase();
152
+ if (fallbackOut.includes('success')) {
153
+ ui.log.success(t('reset.success.uninstalled'));
154
+ return true;
155
+ }
156
+ if (fallbackOut.includes('not installed') || fallbackOut.includes('unknown package')) {
157
+ ui.log.message(kleur.dim(`– ${t('reset.info.notInstalled')}`));
158
+ return true;
159
+ }
160
+
161
+ ui.log.warn(
162
+ res.stdout || res.stderr || fallback.stdout || fallback.stderr ||
163
+ t('reset.warn.androidUninstallFailed')
164
+ );
165
+ return false;
166
+ }
167
+
168
+ /// Reads the package name of the device's default home launcher.
169
+ /// Used so we can clear ITS cache (not the app's) — the launcher caches
170
+ /// widget previews aggressively and a gray preview persists across
171
+ /// uninstalls of the app itself.
172
+ function detectAndroidLauncher(device) {
173
+ const res = runCmd('adb', [
174
+ '-s', device.id,
175
+ 'shell', 'cmd', 'package', 'resolve-activity',
176
+ '--brief',
177
+ '-c', 'android.intent.category.HOME',
178
+ '-a', 'android.intent.action.MAIN',
179
+ ]);
180
+ if (res.code !== 0) return null;
181
+ // Output ends with a line like:
182
+ // com.google.android.apps.nexuslauncher/.NexusLauncherActivity
183
+ const lines = res.stdout.split('\n').map((l) => l.trim()).filter(Boolean);
184
+ for (let i = lines.length - 1; i >= 0; i--) {
185
+ const line = lines[i];
186
+ if (line.includes('/') && !line.startsWith('priority=')) {
187
+ return line.split('/')[0];
188
+ }
189
+ }
190
+ return null;
191
+ }
192
+
193
+ /// Clears the launcher's preview cache and force-stops it.
194
+ /// pm clear-cache removes cached widget previews from disk; force-stop
195
+ /// kicks any in-memory composition. Neither resets the user's home
196
+ /// screen — that would be `pm clear` (which we intentionally do NOT do).
197
+ function clearAndroidLauncherCache(device, launcherPkg, t) {
198
+ ui.log.message(
199
+ kleur.dim(`adb -s ${device.id} shell pm clear-cache ${launcherPkg}`)
200
+ );
201
+ runCmd('adb', [
202
+ '-s', device.id, 'shell', 'pm', 'clear-cache', launcherPkg,
203
+ ]);
204
+ ui.log.message(
205
+ kleur.dim(`adb -s ${device.id} shell am force-stop ${launcherPkg}`)
206
+ );
207
+ const stop = runCmd('adb', [
208
+ '-s', device.id, 'shell', 'am', 'force-stop', launcherPkg,
209
+ ]);
210
+ if (stop.code !== 0) {
211
+ ui.log.warn(t('reset.warn.launcherCacheFailed'));
212
+ return false;
213
+ }
214
+ ui.log.success(t('reset.success.launcherCleared', { pkg: launcherPkg }));
215
+ return true;
216
+ }
217
+
218
+ function noticeIosPhysical(t) {
219
+ ui.log.warn(t('reset.warn.iosDeviceManual'));
220
+ }
221
+
222
+ function isXcodeRunning() {
223
+ const res = spawnSync('pgrep', ['-x', 'Xcode'], { encoding: 'utf8' });
224
+ return res.status === 0 && (res.stdout || '').trim().length > 0;
225
+ }
226
+
227
+ async function ensureXcodeClosed(t) {
228
+ if (!isXcodeRunning()) return true;
229
+ const choice = await ui.select({
230
+ message: t('reset.prompt.xcodeOpen'),
231
+ options: [
232
+ { value: 'close', label: t('reset.prompt.xcodeOpen.close') },
233
+ { value: 'skip', label: t('reset.prompt.xcodeOpen.skip') },
234
+ { value: 'cancel', label: t('reset.prompt.xcodeOpen.cancel') },
235
+ ],
236
+ initialValue: 'close',
237
+ });
238
+ if (choice === 'cancel') return false;
239
+ if (choice === 'close') {
240
+ spawnSync('osascript', ['-e', 'tell application "Xcode" to quit'], {
241
+ encoding: 'utf8',
242
+ });
243
+ // Give Xcode a moment to release the debug session before we proceed.
244
+ await new Promise((resolve) => setTimeout(resolve, 1500));
245
+ ui.log.message(kleur.dim(t('reset.info.xcodeClosed')));
246
+ }
247
+ return true;
248
+ }
249
+
250
+ function noticeWeb(t) {
251
+ ui.log.warn(t('reset.warn.webIncognito'));
252
+ }
253
+
254
+ function runFlutterOnDevice(device, projectDir, t) {
255
+ return spawnFlutterWithSpinner(['run', '-d', device.id], projectDir, t);
256
+ }
257
+
258
+ async function runReset(directory, options = {}) {
259
+ const t = createTranslator(options.language || detectDefaultLanguage());
260
+ const projectDir = path.resolve(directory || '.');
261
+
262
+ if (!(await fs.pathExists(path.join(projectDir, 'pubspec.yaml')))) {
263
+ throw new Error(t('reset.error.notFlutterProject'));
264
+ }
265
+
266
+ printCompactHeader(t);
267
+ ui.intro(t('reset.title'));
268
+
269
+ const [bundleId, packageName] = await Promise.all([
270
+ readBundleId(projectDir),
271
+ readPackageName(projectDir),
272
+ ]);
273
+ if (!bundleId && !packageName) {
274
+ throw new Error(t('reset.error.noIdentifier'));
275
+ }
276
+
277
+ ui.log.message(`${kleur.dim('iOS bundle id:')} ${bundleId || kleur.dim('—')}`);
278
+ ui.log.message(`${kleur.dim('Android package:')} ${packageName || kleur.dim('—')}`);
279
+
280
+ const scanSpinner = ui.spinner();
281
+ scanSpinner.start(t('reset.scanning'));
282
+ const allDevices = await listFlutterDevices(projectDir);
283
+ const devices = filterDevices(allDevices, options);
284
+ scanSpinner.stop(t('reset.scanning'));
285
+
286
+ if (devices.length === 0) {
287
+ ui.log.warn(t('reset.warn.noDevices'));
288
+ return;
289
+ }
290
+
291
+ const target = await pickDevice(devices, t);
292
+ if (!target) {
293
+ ui.log.warn(t('reset.warn.nothingSelected'));
294
+ return;
295
+ }
296
+
297
+ const kind = classifyTarget(target);
298
+
299
+ if (kind === 'ios-device') {
300
+ const proceed = await ensureXcodeClosed(t);
301
+ if (!proceed) {
302
+ ui.outro(t('reset.outro.cancelled'));
303
+ return;
304
+ }
305
+ }
306
+
307
+ ui.log.step(kleur.bold(`${t('reset.resetting')}: ${target.name}`));
308
+
309
+ let didReset = false;
310
+ switch (kind) {
311
+ case 'ios-simulator':
312
+ if (!bundleId) {
313
+ ui.log.error(t('reset.error.noBundleId'));
314
+ break;
315
+ }
316
+ didReset = resetIosSimulator(target, bundleId, t);
317
+ break;
318
+ case 'ios-device':
319
+ if (!bundleId) {
320
+ ui.log.error(t('reset.error.noBundleId'));
321
+ break;
322
+ }
323
+ didReset = resetIosDevice(target, bundleId, t);
324
+ break;
325
+ case 'android-emulator':
326
+ case 'android-device':
327
+ if (!packageName) {
328
+ ui.log.error(t('reset.error.noPackageName'));
329
+ break;
330
+ }
331
+ didReset = resetAndroid(target, packageName, t);
332
+ break;
333
+ case 'web':
334
+ noticeWeb(t);
335
+ break;
336
+ default:
337
+ ui.log.warn(`${t('reset.warn.unknownPlatform')}: ${kind}`);
338
+ }
339
+
340
+ if (!didReset) {
341
+ ui.outro(t('reset.outro.skipped'));
342
+ return;
343
+ }
344
+
345
+ // On Android, also clear the launcher cache so widget previews refresh.
346
+ // Without this, the gallery keeps showing the gray placeholder even after
347
+ // the app is reinstalled (the launcher caches preview drawables by
348
+ // package + component name, which don't change between installs).
349
+ if (
350
+ (kind === 'android-emulator' || kind === 'android-device') &&
351
+ options.clearLauncher !== false
352
+ ) {
353
+ const launcherPkg = detectAndroidLauncher(target);
354
+ if (launcherPkg) {
355
+ const proceedClear = await ui.confirm({
356
+ message: t('reset.prompt.clearLauncherCache', { pkg: launcherPkg }),
357
+ initialValue: true,
358
+ });
359
+ if (proceedClear) {
360
+ clearAndroidLauncherCache(target, launcherPkg, t);
361
+ }
362
+ } else {
363
+ ui.log.message(kleur.dim(t('reset.warn.launcherNotDetected')));
364
+ }
365
+ }
366
+
367
+ if (options.reinstall === false) {
368
+ ui.outro(t('reset.outro.uninstalledOnly'));
369
+ return;
370
+ }
371
+
372
+ const proceed = await ui.confirm({
373
+ message: t('reset.prompt.reinstallNow'),
374
+ initialValue: true,
375
+ });
376
+ if (!proceed) {
377
+ ui.outro(t('reset.outro.uninstalledOnly'));
378
+ return;
379
+ }
380
+
381
+ ui.log.step(kleur.bold(t('reset.reinstalling')));
382
+ await runFlutterOnDevice(target, projectDir, t);
383
+ }
384
+
385
+ module.exports = { runReset };
@@ -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
- return new Promise((resolve, reject) => {
63
- const proc = spawn('flutter', args, { cwd: projectDir, stdio: 'inherit' });
64
- proc.on('close', (code) => {
65
- if (code === 0) resolve();
66
- else reject(new Error(`flutter run exited with code ${code}`));
67
- });
68
- proc.on('error', (err) => {
69
- if (err.code === 'ENOENT') {
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 };
@@ -7,12 +7,15 @@ const kleur = require('kleur');
7
7
  const ui = require('../utils/ui');
8
8
  const { printCompactHeader } = require('../utils/brand');
9
9
  const { createTranslator, detectDefaultLanguage } = require('../utils/i18n');
10
+ const { writeAndroid12Variant } = require('../utils/png-padding');
10
11
 
11
12
  const execAsync = promisify(exec);
12
13
 
13
14
  const ASSETS_DIR = path.join('assets', 'images');
14
15
  const LIGHT_NAME = 'splash_logo_light.png';
15
16
  const DARK_NAME = 'splash_logo_dark.png';
17
+ const LIGHT_ANDROID12_NAME = 'splash_logo_light_android12.png';
18
+ const DARK_ANDROID12_NAME = 'splash_logo_dark_android12.png';
16
19
 
17
20
  const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
18
21
 
@@ -76,9 +79,8 @@ async function assertKasyProject(projectDir, t) {
76
79
 
77
80
  /**
78
81
  * @param {string} flagValue
79
- * @param {string} role 'light' | 'dark'
80
82
  */
81
- function resolveInputPath(flagValue, role) {
83
+ function resolveInputPath(flagValue) {
82
84
  if (!flagValue) return null;
83
85
  const expanded = flagValue.startsWith('~')
84
86
  ? path.join(require('node:os').homedir(), flagValue.slice(1))
@@ -99,8 +101,8 @@ async function runSplash(projectDir, options = {}) {
99
101
 
100
102
  await assertKasyProject(projectDir, t);
101
103
 
102
- const lightPath = resolveInputPath(options.light, 'light');
103
- const darkPath = resolveInputPath(options.dark, 'dark');
104
+ const lightPath = resolveInputPath(options.light);
105
+ const darkPath = resolveInputPath(options.dark);
104
106
 
105
107
  if (!lightPath || !darkPath) {
106
108
  ui.log.error(t('splash.error.bothRequired'));
@@ -160,6 +162,8 @@ async function runSplash(projectDir, options = {}) {
160
162
 
161
163
  const destLight = path.join(projectDir, ASSETS_DIR, LIGHT_NAME);
162
164
  const destDark = path.join(projectDir, ASSETS_DIR, DARK_NAME);
165
+ const destLightA12 = path.join(projectDir, ASSETS_DIR, LIGHT_ANDROID12_NAME);
166
+ const destDarkA12 = path.join(projectDir, ASSETS_DIR, DARK_ANDROID12_NAME);
163
167
 
164
168
  const copySpinner = ui.spinner();
165
169
  copySpinner.start(t('splash.copying'));
@@ -167,6 +171,12 @@ async function runSplash(projectDir, options = {}) {
167
171
  await fs.copy(darkPath, destDark, { overwrite: true });
168
172
  copySpinner.stop(t('splash.copied'));
169
173
 
174
+ const a12Spinner = ui.spinner();
175
+ a12Spinner.start(t('splash.android12Generating'));
176
+ await writeAndroid12Variant(destLight, destLightA12);
177
+ await writeAndroid12Variant(destDark, destDarkA12);
178
+ a12Spinner.stop(t('splash.android12Generated'));
179
+
170
180
  if (options.skipGenerate) {
171
181
  ui.note(t('splash.skipGenerate.hint'), t('splash.skipGenerate.title'));
172
182
  ui.outro(t('splash.done'));
@@ -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.spinner();
377
+ const spinner = ui.timedSpinner();
378
378
  spinner.start(t('update.buildRunner'));
379
379
  try {
380
380
  await execAsync(
@@ -1,4 +1,4 @@
1
- # AppFirebase
1
+ # Kasy App
2
2
 
3
3
  Flutter app com backend API REST — gerado pelo kasy.
4
4
 
@@ -14,7 +14,7 @@
14
14
  <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
15
15
 
16
16
  <application
17
- android:label="AppFirebase"
17
+ android:label="Kasy App"
18
18
  android:name="${applicationName}"
19
19
  android:icon="@mipmap/ic_launcher">
20
20
  <activity