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.
- package/bin/kasy.js +18 -5
- package/lib/commands/icon.js +29 -1
- package/lib/commands/ios.js +8 -2
- package/lib/commands/reset.js +100 -2
- package/lib/commands/run.js +61 -2
- package/lib/commands/splash.js +11 -0
- package/lib/scaffold/backends/api/pubspec.yaml.tpl +4 -2
- package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +4 -2
- package/lib/utils/apple-release.js +30 -0
- package/lib/utils/checks.js +41 -2
- package/lib/utils/debug.js +75 -0
- package/lib/utils/friendly-error.js +91 -0
- package/lib/utils/i18n/messages-en.js +977 -0
- package/lib/utils/i18n/messages-es.js +975 -0
- package/lib/utils/i18n/messages-pt.js +975 -0
- package/lib/utils/i18n.js +21 -2818
- package/lib/utils/png-padding.js +252 -0
- package/package.json +8 -3
- package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/MyWidget.kt +12 -11
- package/templates/firebase/android/app/src/main/res/drawable-hdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-hdpi/ic_launcher_background.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.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/ic_launcher_background.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-hdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-mdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-xhdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-night-xxhdpi/android12splash.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-xhdpi/android12splash.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-xhdpi/ic_launcher_background.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.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/ic_launcher_background.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.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/ic_launcher_background.png +0 -0
- package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png +0 -0
- package/templates/firebase/android/app/src/main/res/layout/widget_preview.xml +18 -11
- package/templates/firebase/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +9 -0
- package/templates/firebase/assets/images/icon_android.png +0 -0
- package/templates/firebase/assets/images/icon_foreground_empty.png +0 -0
- package/templates/firebase/assets/images/splash_logo_dark_android12.png +0 -0
- package/templates/firebase/assets/images/splash_logo_light_android12.png +0 -0
- package/templates/firebase/lib/components/components.dart +1 -0
- package/templates/firebase/lib/components/kasy_avatar.dart +88 -57
- package/templates/firebase/lib/components/kasy_avatar_presets.dart +116 -74
- package/templates/firebase/lib/components/kasy_tabs.dart +431 -0
- package/templates/firebase/lib/core/home_widgets/home_widget_mywidget_service.dart +12 -6
- package/templates/firebase/lib/features/home/home_components_page.dart +1 -1
- package/templates/firebase/lib/features/home/home_components_preview_registry.dart +316 -93
- package/templates/firebase/pubspec.yaml +4 -2
- 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
|
-
|
|
723
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
});
|
package/lib/commands/icon.js
CHANGED
|
@@ -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
|
|
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'));
|
package/lib/commands/ios.js
CHANGED
|
@@ -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
|
-
|
|
184
|
-
|
|
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
|
}
|
package/lib/commands/reset.js
CHANGED
|
@@ -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
|
-
|
|
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;
|
package/lib/commands/run.js
CHANGED
|
@@ -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}`);
|
package/lib/commands/splash.js
CHANGED
|
@@ -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/
|
|
118
|
-
image_dark: assets/images/
|
|
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/
|
|
120
|
-
image_dark: assets/images/
|
|
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
|
};
|
package/lib/utils/checks.js
CHANGED
|
@@ -90,7 +90,7 @@ const FIREBASE_CHECKS = [
|
|
|
90
90
|
command: 'gcloud --version',
|
|
91
91
|
required: false,
|
|
92
92
|
failHint: getGcloudInstallHint(),
|
|
93
|
-
|
|
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
|
-
|
|
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
|
+
};
|