kasy-cli 1.14.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.
- package/bin/kasy.js +18 -5
- package/lib/commands/ios.js +8 -2
- package/lib/commands/reset.js +100 -2
- package/lib/commands/splash.js +11 -0
- package/lib/scaffold/backends/api/pubspec.yaml.tpl +2 -2
- package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +2 -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 +970 -0
- package/lib/utils/i18n/messages-es.js +968 -0
- package/lib/utils/i18n/messages-pt.js +968 -0
- package/lib/utils/i18n.js +21 -2818
- package/lib/utils/png-padding.js +120 -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 +6 -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/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/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'));
|
|
@@ -114,5 +114,5 @@ flutter_native_splash:
|
|
|
114
114
|
android_12:
|
|
115
115
|
color: "#FFFFFF"
|
|
116
116
|
color_dark: "#000000"
|
|
117
|
-
image: assets/images/
|
|
118
|
-
image_dark: assets/images/
|
|
117
|
+
image: assets/images/splash_logo_light_android12.png
|
|
118
|
+
image_dark: assets/images/splash_logo_dark_android12.png
|
|
@@ -116,5 +116,5 @@ flutter_native_splash:
|
|
|
116
116
|
android_12:
|
|
117
117
|
color: "#FFFFFF"
|
|
118
118
|
color_dark: "#000000"
|
|
119
|
-
image: assets/images/
|
|
120
|
-
image_dark: assets/images/
|
|
119
|
+
image: assets/images/splash_logo_light_android12.png
|
|
120
|
+
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
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Map common errors to actionable hints for non-technical users.
|
|
3
|
+
*
|
|
4
|
+
* Pattern: takes an Error and the current translator (t), returns a string
|
|
5
|
+
* with both the original message and a one-line "→ try this" suggestion.
|
|
6
|
+
* Falls back to just the message when no pattern matches.
|
|
7
|
+
*
|
|
8
|
+
* Used by bin/kasy.js global error handler.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
const kleur = require('kleur');
|
|
14
|
+
|
|
15
|
+
// Each rule: { match: RegExp | (err) => boolean, hintKey: i18n key, hintFallback: english fallback }
|
|
16
|
+
// Order matters — more specific rules first.
|
|
17
|
+
const RULES = [
|
|
18
|
+
{
|
|
19
|
+
match: /pubspec\.yaml/i,
|
|
20
|
+
hintKey: 'error.hint.notFlutterProject',
|
|
21
|
+
hintFallback: 'You\'re not inside a Flutter project. Try `kasy new` to create one, or cd into an existing one.',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
match: /flutter[^a-z]+not found|ENOENT.*flutter/i,
|
|
25
|
+
hintKey: 'error.hint.flutterMissing',
|
|
26
|
+
hintFallback: 'Flutter is not installed or not on your PATH. Run `kasy doctor` to diagnose.',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
match: /EACCES|permission denied/i,
|
|
30
|
+
hintKey: 'error.hint.permission',
|
|
31
|
+
hintFallback: 'A file or folder is read-only. Check the parent directory permissions or try again from your home folder.',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
match: /ENOSPC|No space left/i,
|
|
35
|
+
hintKey: 'error.hint.noSpace',
|
|
36
|
+
hintFallback: 'Disk is full. Free up space (Flutter/Xcode builds need 5-15 GB) and try again.',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
match: /ECONNREFUSED|ENOTFOUND|getaddrinfo|network/i,
|
|
40
|
+
hintKey: 'error.hint.network',
|
|
41
|
+
hintFallback: 'Network problem — check your internet connection and try again.',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
match: /firebase.*not.*log(ged|in)|firebase login/i,
|
|
45
|
+
hintKey: 'error.hint.firebaseLogin',
|
|
46
|
+
hintFallback: 'You are not logged into Firebase. Run: firebase login',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
match: /supabase.*not.*log(ged|in)|supabase login/i,
|
|
50
|
+
hintKey: 'error.hint.supabaseLogin',
|
|
51
|
+
hintFallback: 'You are not logged into Supabase. Run: supabase login',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
match: /gcloud.*not authenticated|gcloud auth/i,
|
|
55
|
+
hintKey: 'error.hint.gcloudAuth',
|
|
56
|
+
hintFallback: 'You need to authenticate with gcloud. Run: gcloud auth login',
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
match: /flutter run exited|flutter exited/i,
|
|
60
|
+
hintKey: 'error.hint.flutterRunFailed',
|
|
61
|
+
hintFallback: 'Flutter could not run the app. Run again with --verbose to see the full Flutter output.',
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
function findHint(message) {
|
|
66
|
+
const text = String(message || '');
|
|
67
|
+
for (const rule of RULES) {
|
|
68
|
+
const isMatch = typeof rule.match === 'function' ? rule.match({ message: text }) : rule.match.test(text);
|
|
69
|
+
if (isMatch) return rule;
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Format an error for terminal output. Returns a string with the red ✗ line
|
|
76
|
+
* plus, when we recognize the pattern, a dim hint line beneath it.
|
|
77
|
+
*/
|
|
78
|
+
function formatError(err, t) {
|
|
79
|
+
const message = err && err.message ? err.message : String(err);
|
|
80
|
+
const lines = [kleur.red(`\n✗ ${message}`)];
|
|
81
|
+
const hint = findHint(message);
|
|
82
|
+
if (hint) {
|
|
83
|
+
const text = t ? t(hint.hintKey) : null;
|
|
84
|
+
// Translator returns the key itself when missing — fall back to English.
|
|
85
|
+
const hintText = (text && text !== hint.hintKey) ? text : hint.hintFallback;
|
|
86
|
+
lines.push(kleur.dim(` → ${hintText}`));
|
|
87
|
+
}
|
|
88
|
+
return lines.join('\n');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
module.exports = { formatError, findHint };
|