kasy-cli 1.14.0 → 1.16.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 (54) hide show
  1. package/bin/kasy.js +18 -5
  2. package/lib/commands/icon.js +29 -1
  3. package/lib/commands/ios.js +8 -2
  4. package/lib/commands/reset.js +100 -2
  5. package/lib/commands/run.js +61 -2
  6. package/lib/commands/splash.js +11 -0
  7. package/lib/scaffold/backends/api/pubspec.yaml.tpl +4 -2
  8. package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +4 -2
  9. package/lib/utils/apple-release.js +30 -0
  10. package/lib/utils/checks.js +41 -2
  11. package/lib/utils/debug.js +75 -0
  12. package/lib/utils/friendly-error.js +91 -0
  13. package/lib/utils/i18n/messages-en.js +977 -0
  14. package/lib/utils/i18n/messages-es.js +975 -0
  15. package/lib/utils/i18n/messages-pt.js +975 -0
  16. package/lib/utils/i18n.js +21 -2818
  17. package/lib/utils/png-padding.js +252 -0
  18. package/package.json +8 -3
  19. package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/MyWidget.kt +12 -11
  20. package/templates/firebase/android/app/src/main/res/drawable-hdpi/android12splash.png +0 -0
  21. package/templates/firebase/android/app/src/main/res/drawable-hdpi/ic_launcher_background.png +0 -0
  22. package/templates/firebase/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png +0 -0
  23. package/templates/firebase/android/app/src/main/res/drawable-mdpi/android12splash.png +0 -0
  24. package/templates/firebase/android/app/src/main/res/drawable-mdpi/ic_launcher_background.png +0 -0
  25. package/templates/firebase/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png +0 -0
  26. package/templates/firebase/android/app/src/main/res/drawable-night-hdpi/android12splash.png +0 -0
  27. package/templates/firebase/android/app/src/main/res/drawable-night-mdpi/android12splash.png +0 -0
  28. package/templates/firebase/android/app/src/main/res/drawable-night-xhdpi/android12splash.png +0 -0
  29. package/templates/firebase/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png +0 -0
  30. package/templates/firebase/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png +0 -0
  31. package/templates/firebase/android/app/src/main/res/drawable-xhdpi/android12splash.png +0 -0
  32. package/templates/firebase/android/app/src/main/res/drawable-xhdpi/ic_launcher_background.png +0 -0
  33. package/templates/firebase/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png +0 -0
  34. package/templates/firebase/android/app/src/main/res/drawable-xxhdpi/android12splash.png +0 -0
  35. package/templates/firebase/android/app/src/main/res/drawable-xxhdpi/ic_launcher_background.png +0 -0
  36. package/templates/firebase/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png +0 -0
  37. package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/android12splash.png +0 -0
  38. package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_background.png +0 -0
  39. package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png +0 -0
  40. package/templates/firebase/android/app/src/main/res/layout/widget_preview.xml +18 -11
  41. package/templates/firebase/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +9 -0
  42. package/templates/firebase/assets/images/icon_android.png +0 -0
  43. package/templates/firebase/assets/images/icon_foreground_empty.png +0 -0
  44. package/templates/firebase/assets/images/splash_logo_dark_android12.png +0 -0
  45. package/templates/firebase/assets/images/splash_logo_light_android12.png +0 -0
  46. package/templates/firebase/lib/components/components.dart +1 -0
  47. package/templates/firebase/lib/components/kasy_avatar.dart +88 -57
  48. package/templates/firebase/lib/components/kasy_avatar_presets.dart +116 -74
  49. package/templates/firebase/lib/components/kasy_tabs.dart +431 -0
  50. package/templates/firebase/lib/core/home_widgets/home_widget_mywidget_service.dart +12 -6
  51. package/templates/firebase/lib/features/home/home_components_page.dart +1 -1
  52. package/templates/firebase/lib/features/home/home_components_preview_registry.dart +316 -93
  53. package/templates/firebase/pubspec.yaml +4 -2
  54. package/templates/firebase/web/index.html +9 -0
package/bin/kasy.js CHANGED
@@ -41,6 +41,8 @@ const { ensureLicenseKey, shouldRequireLicenseForArgv } = require('../lib/utils/
41
41
  const { checkForUpdates } = require('../lib/utils/updates');
42
42
  const { printCompactHeader } = require('../lib/utils/brand');
43
43
  const ui = require('../lib/utils/ui');
44
+ const { stripVerboseFlag, isVerbose, printVerboseError } = require('../lib/utils/debug');
45
+ const { formatError } = require('../lib/utils/friendly-error');
44
46
 
45
47
  function createLocalizedHelpConfig(t) {
46
48
  return {
@@ -360,6 +362,7 @@ function buildProgram(language) {
360
362
  .option('--web', 'Show web reset instructions only')
361
363
  .option('-d, --device <id>', 'Reset specific device ID')
362
364
  .option('--no-reinstall', 'Only uninstall, skip reinstall')
365
+ .option('--no-clear-launcher', 'Android only: skip clearing the launcher cache (widget previews may stay gray)')
363
366
  .description(t('cli.command.reset.description'))
364
367
  .action(async (directory, options) => {
365
368
  await runReset(directory, { language, ...options });
@@ -718,18 +721,28 @@ async function resolveLanguage(argv) {
718
721
  return detectDefaultLanguage();
719
722
  }
720
723
 
724
+ // Track the resolved language so the global catch can localize hints.
725
+ let activeLanguage = null;
726
+
721
727
  async function main() {
722
- const argv = process.argv.slice(2);
723
- const language = await resolveLanguage(argv);
728
+ // Strip --verbose before commander sees it — that flag is handled out-of-band
729
+ // by lib/utils/debug.js (reads process.argv directly).
730
+ const argv = stripVerboseFlag(process.argv.slice(2));
731
+ activeLanguage = await resolveLanguage(argv);
724
732
  if (shouldRequireLicenseForArgv(argv)) {
725
- await ensureLicenseKey({ language, argv });
733
+ await ensureLicenseKey({ language: activeLanguage, argv });
726
734
  }
727
735
  await checkForUpdates();
728
- const program = buildProgram(language);
736
+ const program = buildProgram(activeLanguage);
729
737
  await program.parseAsync([process.argv[0], process.argv[1], ...argv]);
730
738
  }
731
739
 
732
740
  main().catch((error) => {
733
- console.error(kleur.red(`\n✗ ${error.message}`));
741
+ const t = activeLanguage ? createTranslator(activeLanguage) : null;
742
+ console.error(formatError(error, t));
743
+ printVerboseError(error);
744
+ if (!isVerbose()) {
745
+ console.error(kleur.dim(' Run again with --verbose (or set KASY_DEBUG=1) for full output.'));
746
+ }
734
747
  process.exitCode = 1;
735
748
  });
@@ -7,11 +7,17 @@ const ui = require('../utils/ui');
7
7
  const { printCompactHeader } = require('../utils/brand');
8
8
  const { createTranslator, detectDefaultLanguage } = require('../utils/i18n');
9
9
  const { inspectPng } = require('./splash');
10
+ const {
11
+ writeAndroidAdaptiveBackground,
12
+ writeTransparentSquare,
13
+ } = require('../utils/png-padding');
10
14
 
11
15
  const execAsync = promisify(exec);
12
16
 
13
17
  const ASSETS_DIR = path.join('assets', 'images');
14
18
  const ICON_NAME = 'icon.png';
19
+ const ANDROID_BG_NAME = 'icon_android.png';
20
+ const ANDROID_FG_EMPTY_NAME = 'icon_foreground_empty.png';
15
21
 
16
22
  async function assertKasyProject(projectDir, t) {
17
23
  const kitSetupPath = path.join(projectDir, 'kit_setup.json');
@@ -85,13 +91,35 @@ async function runIcon(projectDir, options = {}) {
85
91
  ui.note(warnings.map((w) => `${kleur.yellow('⚠')} ${w}`).join('\n'), t('icon.warn.title'));
86
92
  }
87
93
 
88
- const dest = path.join(projectDir, ASSETS_DIR, ICON_NAME);
94
+ const assetsDir = path.join(projectDir, ASSETS_DIR);
95
+ const dest = path.join(assetsDir, ICON_NAME);
96
+ const androidBgDest = path.join(assetsDir, ANDROID_BG_NAME);
97
+ const androidFgEmptyDest = path.join(assetsDir, ANDROID_FG_EMPTY_NAME);
89
98
 
90
99
  const copySpinner = ui.spinner();
91
100
  copySpinner.start(t('icon.copying'));
92
101
  await fs.copy(imagePath, dest, { overwrite: true });
93
102
  copySpinner.stop(t('icon.copied'));
94
103
 
104
+ const adaptiveSpinner = ui.spinner();
105
+ adaptiveSpinner.start(t('icon.adaptive.generating'));
106
+ let adaptiveInfo;
107
+ try {
108
+ adaptiveInfo = await writeAndroidAdaptiveBackground(dest, androidBgDest);
109
+ if (!(await fs.pathExists(androidFgEmptyDest))) {
110
+ await writeTransparentSquare(androidFgEmptyDest);
111
+ }
112
+ adaptiveSpinner.stop(t('icon.adaptive.generated'));
113
+ } catch (err) {
114
+ adaptiveSpinner.stop(`⚠ ${t('icon.adaptive.failed')}`);
115
+ ui.log.message(kleur.dim(err.message || String(err)));
116
+ process.exit(1);
117
+ }
118
+
119
+ if (!adaptiveInfo.color.sampled) {
120
+ ui.note(t('icon.adaptive.fallbackColor'), t('icon.adaptive.fallbackTitle'));
121
+ }
122
+
95
123
  if (options.skipGenerate) {
96
124
  ui.note(t('icon.skipGenerate.hint'), t('icon.skipGenerate.title'));
97
125
  ui.outro(t('icon.done'));
@@ -19,7 +19,9 @@ const {
19
19
  checkIosSigning,
20
20
  checkDiskSpaceForIosBuild,
21
21
  isXcodeCacheBuildError,
22
+ isPodNetworkError,
22
23
  printBuildFailureHints,
24
+ printPodNetworkHints,
23
25
  runIosClean,
24
26
  MIN_DISK_GB_FOR_IOS_BUILD,
25
27
  URL_APP_STORE_CONNECT_APPS,
@@ -180,9 +182,13 @@ async function runBuildOrRelease(directory, options = {}, mode) {
180
182
  ui.outro(mode === 'build' ? t('ios.build.success') : t('ios.release.success'));
181
183
  } catch (err) {
182
184
  const output = err.buildOutput || err.message;
183
- ui.log.error(output.slice(-4000));
184
- if (isXcodeCacheBuildError(output)) {
185
+ if (isPodNetworkError(output)) {
186
+ printPodNetworkHints(t);
187
+ } else if (isXcodeCacheBuildError(output)) {
188
+ ui.log.error(output.slice(-4000));
185
189
  printBuildFailureHints(t, projectDir);
190
+ } else {
191
+ ui.log.error(output.slice(-4000));
186
192
  }
187
193
  process.exitCode = 1;
188
194
  }
@@ -123,9 +123,13 @@ function resetIosDevice(device, bundleId, t) {
123
123
  }
124
124
 
125
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
+
126
131
  ui.log.message(kleur.dim(`adb -s ${device.id} uninstall ${packageName}`));
127
132
  const res = runCmd('adb', ['-s', device.id, 'uninstall', packageName]);
128
- // adb prints "Success" or "Failure [DELETE_FAILED_INTERNAL_ERROR]" / "not installed"
129
133
  const out = `${res.stdout}\n${res.stderr}`.toLowerCase();
130
134
  if (out.includes('success')) {
131
135
  ui.log.success(t('reset.success.uninstalled'));
@@ -135,10 +139,82 @@ function resetAndroid(device, packageName, t) {
135
139
  ui.log.message(kleur.dim(`– ${t('reset.info.notInstalled')}`));
136
140
  return true;
137
141
  }
138
- ui.log.warn(res.stdout || res.stderr || t('reset.warn.androidUninstallFailed'));
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
+ );
139
165
  return false;
140
166
  }
141
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
+
142
218
  function noticeIosPhysical(t) {
143
219
  ui.log.warn(t('reset.warn.iosDeviceManual'));
144
220
  }
@@ -266,6 +342,28 @@ async function runReset(directory, options = {}) {
266
342
  return;
267
343
  }
268
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
+
269
367
  if (options.reinstall === false) {
270
368
  ui.outro(t('reset.outro.uninstalledOnly'));
271
369
  return;
@@ -1,10 +1,52 @@
1
1
  const path = require('node:path');
2
+ const { spawnSync } = require('node:child_process');
2
3
  const fs = require('fs-extra');
3
4
  const kleur = require('kleur');
5
+ const ui = require('../utils/ui');
4
6
  const { createTranslator, detectDefaultLanguage } = require('../utils/i18n');
5
7
  const { printCompactHeader } = require('../utils/brand');
6
8
  const { spawnFlutterWithSpinner } = require('../utils/flutter-run');
7
9
 
10
+ function listFlutterDevices(projectDir) {
11
+ const res = spawnSync('flutter', ['devices', '--machine'], {
12
+ cwd: projectDir,
13
+ encoding: 'utf8',
14
+ });
15
+ if (res.status !== 0) return [];
16
+ try {
17
+ const parsed = JSON.parse(res.stdout);
18
+ return Array.isArray(parsed) ? parsed : [];
19
+ } catch {
20
+ return [];
21
+ }
22
+ }
23
+
24
+ function classifyDevice(device) {
25
+ const platform = (device.targetPlatform || '').toLowerCase();
26
+ if (platform === 'ios') return device.emulator ? 'ios-simulator' : 'ios-device';
27
+ if (platform.startsWith('android')) {
28
+ return device.emulator ? 'android-emulator' : 'android-device';
29
+ }
30
+ if (platform.startsWith('web')) return 'web';
31
+ if (platform.startsWith('darwin')) return 'macos';
32
+ if (platform.startsWith('linux')) return 'linux';
33
+ if (platform.startsWith('windows')) return 'windows';
34
+ return 'unknown';
35
+ }
36
+
37
+ async function pickDevice(devices, t) {
38
+ if (devices.length === 0) return null;
39
+ if (devices.length === 1) return devices[0];
40
+ const choice = await ui.select({
41
+ message: t('run.prompt.pickDevice'),
42
+ options: devices.map((d) => ({
43
+ value: d.id,
44
+ label: `${d.name} ${kleur.dim(`(${classifyDevice(d)})`)}`,
45
+ })),
46
+ });
47
+ return devices.find((d) => d.id === choice) || null;
48
+ }
49
+
8
50
  /**
9
51
  * Read dart-define args from .vscode/launch.json.
10
52
  * Returns the args array from the matching config (dev or prod).
@@ -35,8 +77,11 @@ async function runRun(directory, options = {}) {
35
77
  throw new Error(t('run.error.notFlutterProject'));
36
78
  }
37
79
 
38
- // Resolve device flag
80
+ // Resolve device flag. If none of the platform shortcuts or -d is set,
81
+ // ask the user when more than one device is available — `flutter run`
82
+ // bails out with "More than one device connected" otherwise.
39
83
  const deviceArgs = [];
84
+ let resolvedDeviceLabel = null;
40
85
  if (options.web) {
41
86
  deviceArgs.push('-d', 'chrome');
42
87
  } else if (options.ios) {
@@ -45,6 +90,20 @@ async function runRun(directory, options = {}) {
45
90
  deviceArgs.push('-d', 'android');
46
91
  } else if (options.device) {
47
92
  deviceArgs.push('-d', options.device);
93
+ } else {
94
+ const devices = listFlutterDevices(projectDir);
95
+ if (devices.length > 1) {
96
+ printCompactHeader(t);
97
+ const picked = await pickDevice(devices, t);
98
+ if (!picked) {
99
+ console.log(kleur.yellow(` ⚠ ${t('run.warn.nothingSelected')}`));
100
+ return;
101
+ }
102
+ deviceArgs.push('-d', picked.id);
103
+ resolvedDeviceLabel = `${picked.name} (${picked.id})`;
104
+ }
105
+ // 0 or 1 device → let flutter handle it; it picks the only one or
106
+ // prints its own "no devices" message.
48
107
  }
49
108
 
50
109
  // Read dart-defines from .vscode/launch.json (skip if --no-defines)
@@ -62,7 +121,7 @@ async function runRun(directory, options = {}) {
62
121
  ? 'ios'
63
122
  : options.android
64
123
  ? 'android'
65
- : options.device || null;
124
+ : options.device || resolvedDeviceLabel || null;
66
125
  const summaryParts = [];
67
126
  if (envValue) summaryParts.push(`ENV=${envValue}`);
68
127
  if (deviceLabel) summaryParts.push(`device: ${deviceLabel}`);
@@ -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
 
@@ -159,6 +162,8 @@ async function runSplash(projectDir, options = {}) {
159
162
 
160
163
  const destLight = path.join(projectDir, ASSETS_DIR, LIGHT_NAME);
161
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);
162
167
 
163
168
  const copySpinner = ui.spinner();
164
169
  copySpinner.start(t('splash.copying'));
@@ -166,6 +171,12 @@ async function runSplash(projectDir, options = {}) {
166
171
  await fs.copy(darkPath, destDark, { overwrite: true });
167
172
  copySpinner.stop(t('splash.copied'));
168
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
+
169
180
  if (options.skipGenerate) {
170
181
  ui.note(t('splash.skipGenerate.hint'), t('splash.skipGenerate.title'));
171
182
  ui.outro(t('splash.done'));
@@ -98,6 +98,8 @@ flutter_launcher_icons:
98
98
  android: ic_launcher
99
99
  ios: true
100
100
  remove_alpha_ios: true
101
+ adaptive_icon_background: assets/images/icon_android.png
102
+ adaptive_icon_foreground: assets/images/icon_foreground_empty.png
101
103
  web:
102
104
  generate: true
103
105
  image_path: assets/images/favicon.png
@@ -114,5 +116,5 @@ flutter_native_splash:
114
116
  android_12:
115
117
  color: "#FFFFFF"
116
118
  color_dark: "#000000"
117
- image: assets/images/splash_logo_light.png
118
- image_dark: assets/images/splash_logo_dark.png
119
+ image: assets/images/splash_logo_light_android12.png
120
+ image_dark: assets/images/splash_logo_dark_android12.png
@@ -100,6 +100,8 @@ flutter_launcher_icons:
100
100
  android: ic_launcher
101
101
  ios: true
102
102
  remove_alpha_ios: true
103
+ adaptive_icon_background: assets/images/icon_android.png
104
+ adaptive_icon_foreground: assets/images/icon_foreground_empty.png
103
105
  web:
104
106
  generate: true
105
107
  image_path: assets/images/favicon.png
@@ -116,5 +118,5 @@ flutter_native_splash:
116
118
  android_12:
117
119
  color: "#FFFFFF"
118
120
  color_dark: "#000000"
119
- image: assets/images/splash_logo_light.png
120
- image_dark: assets/images/splash_logo_dark.png
121
+ image: assets/images/splash_logo_light_android12.png
122
+ image_dark: assets/images/splash_logo_dark_android12.png
@@ -185,6 +185,18 @@ const XCODE_CACHE_ERROR_PATTERNS = [
185
185
  /clang: error: no such file or directory:.*\.swiftmodule/i,
186
186
  ];
187
187
 
188
+ const POD_NETWORK_ERROR_PATTERNS = [
189
+ /curl:\s*\(6\)/i,
190
+ /curl:\s*\(7\)/i,
191
+ /curl:\s*\(28\)/i,
192
+ /curl:\s*\(35\)/i,
193
+ /curl:\s*\(56\)/i,
194
+ /Connection reset by peer/i,
195
+ /Could not resolve host/i,
196
+ /Network is unreachable/i,
197
+ /Operation timed out/i,
198
+ ];
199
+
188
200
  async function getFreeDiskGb(checkPath) {
189
201
  if (process.platform !== 'darwin' && process.platform !== 'linux') {
190
202
  return null;
@@ -216,6 +228,11 @@ function isXcodeCacheBuildError(output) {
216
228
  return XCODE_CACHE_ERROR_PATTERNS.some((re) => re.test(text));
217
229
  }
218
230
 
231
+ function isPodNetworkError(output) {
232
+ const text = String(output || '');
233
+ return POD_NETWORK_ERROR_PATTERNS.some((re) => re.test(text));
234
+ }
235
+
219
236
  function printBuildFailureHints(t, projectDir) {
220
237
  const name = path.basename(projectDir);
221
238
  const cleanArg = projectDir !== process.cwd() ? ` ${projectDir}` : '';
@@ -235,6 +252,17 @@ function printBuildFailureHints(t, projectDir) {
235
252
  ui.note(lines.join('\n'), t('ios.hints.title'));
236
253
  }
237
254
 
255
+ function printPodNetworkHints(t) {
256
+ const lines = [
257
+ t('ios.hints.network.body'),
258
+ '',
259
+ `1. ${t('ios.hints.network.step1')}`,
260
+ `2. ${t('ios.hints.network.step2')}: ${kleur.cyan('kasy ios')}`,
261
+ `3. ${t('ios.hints.network.step3')}: ${kleur.cyan('kasy ios clean')}`,
262
+ ];
263
+ ui.note(lines.join('\n'), t('ios.hints.network.title'));
264
+ }
265
+
238
266
  async function runIosClean(projectDir, t) {
239
267
  const steps = [
240
268
  { label: t('ios.clean.step.flutterClean'), cmd: 'flutter clean' },
@@ -349,6 +377,8 @@ module.exports = {
349
377
  getFreeDiskGb,
350
378
  checkDiskSpaceForIosBuild,
351
379
  isXcodeCacheBuildError,
380
+ isPodNetworkError,
352
381
  printBuildFailureHints,
382
+ printPodNetworkHints,
353
383
  runIosClean,
354
384
  };
@@ -90,7 +90,7 @@ const FIREBASE_CHECKS = [
90
90
  command: 'gcloud --version',
91
91
  required: false,
92
92
  failHint: getGcloudInstallHint(),
93
- waitPrompt: 'Após instalar o gcloud, pressione Enter para verificar novamente...',
93
+ waitPromptKey: 'checks.waitPrompt.gcloud.install',
94
94
  },
95
95
  {
96
96
  name: 'gcloud auth (create-from-scratch)',
@@ -98,7 +98,7 @@ const FIREBASE_CHECKS = [
98
98
  required: false,
99
99
  showVersion: false,
100
100
  failHint: 'gcloud auth login',
101
- waitPrompt: 'Após fazer login com gcloud auth login, pressione Enter para verificar...',
101
+ waitPromptKey: 'checks.waitPrompt.gcloud.auth',
102
102
  },
103
103
  ];
104
104
 
@@ -183,6 +183,34 @@ async function runSingleCheck(check, options = {}) {
183
183
  }
184
184
  }
185
185
 
186
+ /**
187
+ * Retry a check after the user installs/auths a missing tool. Used when the
188
+ * check definition has `waitPrompt` — we offer the user to run the install
189
+ * step, then re-check on Enter. Returns true if the recheck succeeded.
190
+ */
191
+ async function retryCheckInteractively(check, t) {
192
+ ui.log.warn(`${check.name} ${t('checks.notFound.short') || 'not found'}`);
193
+ if (check.failHint) {
194
+ ui.log.message(`${t('checks.runHint') || 'Run'}: ${kleur.cyan(check.failHint)}`);
195
+ }
196
+ const proceed = await ui.confirm({
197
+ message: check.waitPrompt,
198
+ initialValue: true,
199
+ });
200
+ if (!proceed) return false;
201
+ try {
202
+ const { stdout } = await execAsync(check.command, { encoding: 'utf8', timeout: TOOL_CHECK_TIMEOUT });
203
+ const version = extractVersion(stdout, check.name);
204
+ ui.log.success(version
205
+ ? `${check.name} — ${version}`
206
+ : check.name);
207
+ return true;
208
+ } catch {
209
+ ui.log.error(`${check.name} ${t('checks.stillMissing') || 'still missing'}`);
210
+ return false;
211
+ }
212
+ }
213
+
186
214
  async function runChecks(checks, title, options = {}) {
187
215
  const t = options.t || createTranslator(options.language || detectDefaultLanguage());
188
216
  const { showVersion = true } = options;
@@ -215,6 +243,17 @@ async function runChecks(checks, title, options = {}) {
215
243
  }
216
244
 
217
245
  for (const result of failures) {
246
+ // Interactive recovery for checks with a waitPrompt (e.g. gcloud install).
247
+ // Lets the user fix the env without restarting the command.
248
+ const waitPromptText = result.waitPromptKey ? t(result.waitPromptKey) : result.waitPrompt;
249
+ if (waitPromptText) {
250
+ const recovered = await retryCheckInteractively({ ...result, waitPrompt: waitPromptText }, t);
251
+ if (recovered) {
252
+ const idx = results.indexOf(result);
253
+ if (idx >= 0) results[idx] = { ...result, ok: true };
254
+ }
255
+ continue;
256
+ }
218
257
  const hint = result.failHint ? `\n${kleur.dim(`→ ${result.failHint}`)}` : '';
219
258
  if (result.required) {
220
259
  const detail = result.autoInstallFailed ? ` — ${t('checks.install.failed')}` : '';
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Debug / verbose helpers.
3
+ *
4
+ * Enabled by either:
5
+ * - `--verbose` flag (set globally in bin/kasy.js via process.argv parsing)
6
+ * - `KASY_DEBUG=1` environment variable
7
+ *
8
+ * Use `debugLog(...)` for diagnostic output that should only show when the
9
+ * user opts in. Use `attachDebugToError(err, context)` before throwing/rejecting
10
+ * to attach extra info (stdout/stderr/command) that surfaces in --verbose mode.
11
+ */
12
+
13
+ 'use strict';
14
+
15
+ const kleur = require('kleur');
16
+
17
+ function isVerbose() {
18
+ if (process.env.KASY_DEBUG === '1' || process.env.KASY_DEBUG === 'true') return true;
19
+ return process.argv.includes('--verbose');
20
+ }
21
+
22
+ /**
23
+ * Strip --verbose from argv before commander parses it, since commander does
24
+ * not know about this flag. Returns a new argv with --verbose removed.
25
+ */
26
+ function stripVerboseFlag(argv) {
27
+ return argv.filter((arg) => arg !== '--verbose');
28
+ }
29
+
30
+ function debugLog(...args) {
31
+ if (!isVerbose()) return;
32
+ console.error(kleur.dim(`[debug] ${args.map(String).join(' ')}`));
33
+ }
34
+
35
+ /**
36
+ * Attach raw command output to an error so --verbose mode can surface it.
37
+ * The original error message stays clean for end users.
38
+ */
39
+ function attachDebugToError(err, { command, stdout, stderr, cwd } = {}) {
40
+ if (!err || typeof err !== 'object') return err;
41
+ err.kasyDebug = {
42
+ command: command || null,
43
+ stdout: stdout != null ? String(stdout) : null,
44
+ stderr: stderr != null ? String(stderr) : null,
45
+ cwd: cwd || null,
46
+ };
47
+ return err;
48
+ }
49
+
50
+ /**
51
+ * Print verbose context for an error if debug is on. Called from the global
52
+ * error handler in bin/kasy.js.
53
+ */
54
+ function printVerboseError(err) {
55
+ if (!isVerbose() || !err) return;
56
+ console.error('');
57
+ console.error(kleur.dim('─── verbose error context ───'));
58
+ if (err.stack) console.error(kleur.dim(err.stack));
59
+ if (err.kasyDebug) {
60
+ const d = err.kasyDebug;
61
+ if (d.command) console.error(kleur.dim(`command: ${d.command}`));
62
+ if (d.cwd) console.error(kleur.dim(`cwd: ${d.cwd}`));
63
+ if (d.stdout) console.error(kleur.dim('stdout:\n' + d.stdout));
64
+ if (d.stderr) console.error(kleur.dim('stderr:\n' + d.stderr));
65
+ }
66
+ console.error(kleur.dim('─────────────────────────────'));
67
+ }
68
+
69
+ module.exports = {
70
+ isVerbose,
71
+ stripVerboseFlag,
72
+ debugLog,
73
+ attachDebugToError,
74
+ printVerboseError,
75
+ };