kasy-cli 1.13.0 → 1.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/kasy.js +122 -7
- package/lib/commands/add.js +2 -2
- package/lib/commands/codemagic.js +11 -4
- package/lib/commands/deploy.js +3 -3
- package/lib/commands/favicon.js +115 -0
- package/lib/commands/icon.js +143 -0
- package/lib/commands/ios.js +20 -5
- package/lib/commands/new.js +8 -20
- package/lib/commands/remove.js +1 -1
- package/lib/commands/reset.js +287 -0
- package/lib/commands/run.js +24 -17
- package/lib/commands/splash.js +3 -4
- package/lib/commands/update.js +1 -1
- package/lib/scaffold/backends/api/patch/README.md +1 -1
- package/lib/scaffold/backends/api/patch/android/app/src/main/AndroidManifest.xml +1 -1
- package/lib/scaffold/backends/api/patch/lib/features/notifications/api/device_api.dart +53 -0
- package/lib/scaffold/backends/api/pubspec.yaml.tpl +11 -1
- package/lib/scaffold/backends/firebase/tokens.js +2 -2
- package/lib/scaffold/backends/supabase/edge-functions/send-push-notification/index.ts +8 -2
- package/lib/scaffold/backends/supabase/migrations/20240101000011_dedupe_device_tokens.sql +34 -0
- package/lib/scaffold/backends/supabase/patch/README.md +1 -1
- package/lib/scaffold/backends/supabase/patch/android/app/src/main/AndroidManifest.xml +1 -1
- package/lib/scaffold/backends/supabase/patch/lib/features/notifications/api/device_api.dart +43 -0
- package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +11 -1
- package/lib/utils/apple-release.js +85 -16
- package/lib/utils/checks.js +4 -105
- package/lib/utils/flutter-run.js +173 -0
- package/lib/utils/i18n.js +335 -0
- package/lib/utils/mobile-identity.js +35 -0
- package/lib/utils/ui.js +114 -0
- package/package.json +1 -2
- package/templates/firebase/README.en.md +1 -1
- package/templates/firebase/README.es.md +1 -1
- package/templates/firebase/README.md +1 -1
- package/templates/firebase/android/app/build.gradle.kts +10 -1
- package/templates/firebase/android/app/src/main/AndroidManifest.xml +1 -1
- package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/MainActivity.kt +25 -1
- package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/MyWidget.kt +160 -11
- package/templates/firebase/android/app/src/main/res/drawable/widget_add_button.xml +15 -0
- package/templates/firebase/android/app/src/main/res/drawable/widget_gradient_bg.xml +9 -0
- package/templates/firebase/android/app/src/main/res/drawable/widget_gradient_inner.xml +12 -0
- package/templates/firebase/android/app/src/main/res/drawable/widget_plan_pill_bg.xml +5 -0
- package/templates/firebase/android/app/src/main/res/drawable/widget_preview_image.xml +17 -0
- package/templates/firebase/android/app/src/main/res/drawable/widget_pro_pill_bg.xml +5 -0
- package/templates/firebase/android/app/src/main/res/layout/widget_loading.xml +8 -0
- package/templates/firebase/android/app/src/main/res/layout/widget_preview.xml +46 -0
- package/templates/firebase/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
- package/templates/firebase/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
- package/templates/firebase/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
- package/templates/firebase/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
- package/templates/firebase/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
- package/templates/firebase/android/app/src/main/res/xml/mywidget_info.xml +9 -3
- package/templates/firebase/assets/images/favicon.png +0 -0
- package/templates/firebase/assets/images/icon.png +0 -0
- package/templates/firebase/firestore.indexes.json +10 -0
- package/templates/firebase/functions/src/core/data/entities/user_device_entity.ts +3 -0
- package/templates/firebase/functions/src/core/data/repositories/user_device_repository.ts +17 -1
- package/templates/firebase/functions/src/index.ts +1 -0
- package/templates/firebase/functions/src/notifications/device_triggers.ts +58 -0
- package/templates/firebase/ios/HomeWidgetExtension/MyWidget.swift +116 -33
- package/templates/firebase/ios/Runner/AppDelegate.swift +17 -1
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png +0 -0
- package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png +0 -0
- package/templates/firebase/ios/Runner/Info.plist +2 -2
- package/templates/firebase/ios/Runner/es.lproj/InfoPlist.strings +1 -1
- package/templates/firebase/ios/Runner/pt-BR.lproj/InfoPlist.strings +1 -1
- package/templates/firebase/ios/Runner/pt.lproj/InfoPlist.strings +1 -1
- package/templates/firebase/lib/components/kasy_button.dart +8 -0
- package/templates/firebase/lib/core/bottom_menu/kasy_bottom_bar_factory.dart +18 -0
- package/templates/firebase/lib/core/home_widgets/home_widget_mywidget_service.dart +67 -19
- package/templates/firebase/lib/core/home_widgets/home_widget_service.dart +22 -8
- package/templates/firebase/lib/core/shared_preferences/shared_preferences.dart +16 -4
- package/templates/firebase/lib/core/states/components/maybeshow_component.dart +4 -8
- package/templates/firebase/lib/core/states/user_state_notifier.dart +13 -1
- package/templates/firebase/lib/features/home/home_page.dart +0 -6
- package/templates/firebase/lib/features/notifications/api/device_api.dart +57 -0
- package/templates/firebase/lib/features/notifications/providers/models/notification.dart +11 -1
- package/templates/firebase/lib/features/notifications/repositories/device_repository.dart +9 -0
- package/templates/firebase/lib/features/notifications/repositories/notifications_repository.dart +1 -4
- package/templates/firebase/lib/features/notifications/shared/att_permission.dart +28 -8
- package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +16 -1
- package/templates/firebase/lib/features/notifications/ui/widgets/empty_notifications.dart +44 -11
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_bottom_sheet.dart +31 -29
- package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +21 -5
- package/templates/firebase/lib/i18n/en.i18n.json +4 -1
- package/templates/firebase/lib/i18n/es.i18n.json +4 -1
- package/templates/firebase/lib/i18n/pt.i18n.json +4 -1
- package/templates/firebase/pubspec.yaml +6 -1
- package/templates/firebase/test/features/notifications/data/device_api_fake.dart +9 -0
- package/templates/firebase/web/favicon.png +0 -0
- package/templates/firebase/web/icons/Icon-192.png +0 -0
- package/templates/firebase/web/icons/Icon-512.png +0 -0
- package/templates/firebase/web/icons/Icon-maskable-192.png +0 -0
- package/templates/firebase/web/icons/Icon-maskable-512.png +0 -0
- package/templates/firebase/web/index.html +3 -0
- package/templates/firebase/web/manifest.json +3 -3
- package/templates/firebase/assets/images/app_icon.png +0 -0
- package/templates/firebase/assets/images/onboarding/img1.jpg +0 -0
- package/templates/firebase/assets/images/onboarding/onboarding.png +0 -0
- package/templates/firebase/lib/core/states/components/maybe_ask_biometric_setup.dart +0 -88
- package/templates/firebase/lib/features/notifications/shared/notification_permission_bottom_sheet.dart +0 -144
|
@@ -5,7 +5,7 @@ const fs = require('fs-extra');
|
|
|
5
5
|
const os = require('node:os');
|
|
6
6
|
const kleur = require('kleur');
|
|
7
7
|
const ui = require('./ui');
|
|
8
|
-
const { exec } = require('node:child_process');
|
|
8
|
+
const { exec, spawn } = require('node:child_process');
|
|
9
9
|
const { promisify } = require('node:util');
|
|
10
10
|
|
|
11
11
|
const execAsync = promisify(exec);
|
|
@@ -43,6 +43,27 @@ async function isKasyFlutterProject(projectDir) {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
async function readBundleId(projectDir) {
|
|
46
|
+
// Source of truth is the Xcode project file — what the build actually uses
|
|
47
|
+
// when installing on a device. kit_setup.json can drift if the user renames
|
|
48
|
+
// the bundle id manually after `kasy new`, so it's only used as a fallback
|
|
49
|
+
// when the iOS project hasn't been generated yet.
|
|
50
|
+
const pbxPath = path.join(projectDir, 'ios', 'Runner.xcodeproj', 'project.pbxproj');
|
|
51
|
+
if (await fs.pathExists(pbxPath)) {
|
|
52
|
+
const content = await fs.readFile(pbxPath, 'utf8');
|
|
53
|
+
// Match the main target only — skip Widget/NotificationService extensions
|
|
54
|
+
// by picking the first identifier that doesn't contain a dot extension.
|
|
55
|
+
const matches = [...content.matchAll(/PRODUCT_BUNDLE_IDENTIFIER = ([^;]+);/g)];
|
|
56
|
+
for (const m of matches) {
|
|
57
|
+
const id = m[1].trim().replace(/"/g, '');
|
|
58
|
+
if (
|
|
59
|
+
!id.endsWith('.HomeWidgetExtension') &&
|
|
60
|
+
!id.endsWith('.NotificationService')
|
|
61
|
+
) {
|
|
62
|
+
return id;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (matches[0]) return matches[0][1].trim().replace(/"/g, '');
|
|
66
|
+
}
|
|
46
67
|
const kitSetupPath = path.join(projectDir, 'kit_setup.json');
|
|
47
68
|
if (await fs.pathExists(kitSetupPath)) {
|
|
48
69
|
try {
|
|
@@ -52,12 +73,6 @@ async function readBundleId(projectDir) {
|
|
|
52
73
|
// ignore
|
|
53
74
|
}
|
|
54
75
|
}
|
|
55
|
-
const pbxPath = path.join(projectDir, 'ios', 'Runner.xcodeproj', 'project.pbxproj');
|
|
56
|
-
if (await fs.pathExists(pbxPath)) {
|
|
57
|
-
const content = await fs.readFile(pbxPath, 'utf8');
|
|
58
|
-
const m = content.match(/PRODUCT_BUNDLE_IDENTIFIER = ([^;]+);/);
|
|
59
|
-
if (m) return m[1].trim().replace(/"/g, '');
|
|
60
|
-
}
|
|
61
76
|
return null;
|
|
62
77
|
}
|
|
63
78
|
|
|
@@ -247,15 +262,69 @@ async function runReleaseScript(projectDir, args, t) {
|
|
|
247
262
|
if (!(await fs.pathExists(scriptPath))) {
|
|
248
263
|
throw new Error(t('ios.error.noScript'));
|
|
249
264
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
265
|
+
|
|
266
|
+
const isBuildOnly = args.includes('--no-upload');
|
|
267
|
+
const titleKey = isBuildOnly ? 'ios.build.task.building' : 'ios.release.task.building';
|
|
268
|
+
const doneKey = isBuildOnly ? 'ios.build.task.done' : 'ios.release.task.done';
|
|
269
|
+
const failKey = isBuildOnly ? 'ios.build.task.failed' : 'ios.release.task.failed';
|
|
270
|
+
|
|
271
|
+
const spinner = ui.timedSpinner();
|
|
272
|
+
spinner.start(t(titleKey));
|
|
273
|
+
|
|
274
|
+
return new Promise((resolve, reject) => {
|
|
275
|
+
const proc = spawn('bash', [scriptPath, ...args], {
|
|
276
|
+
cwd: projectDir,
|
|
277
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
let allOutput = '';
|
|
281
|
+
let uploadStage = false;
|
|
282
|
+
|
|
283
|
+
// Trim noise so the spinner message stays readable. Skip empty lines,
|
|
284
|
+
// pure dotted progress bars (e.g. "....."), and lines that are just
|
|
285
|
+
// separators. Show the last meaningful line as the spinner status.
|
|
286
|
+
const isMeaningful = (line) => {
|
|
287
|
+
if (!line) return false;
|
|
288
|
+
if (/^[.\s]+$/.test(line)) return false;
|
|
289
|
+
if (/^=+$/.test(line)) return false;
|
|
290
|
+
return true;
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const handleChunk = (chunk) => {
|
|
294
|
+
const text = chunk.toString();
|
|
295
|
+
allOutput += text;
|
|
296
|
+
|
|
297
|
+
if (!uploadStage && /Uploading to App Store Connect/i.test(text)) {
|
|
298
|
+
uploadStage = true;
|
|
299
|
+
spinner.message(t('ios.release.task.uploading'));
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const lines = text.split('\n').map((l) => l.replace(/\r/g, '').trim()).filter(isMeaningful);
|
|
304
|
+
const lastLine = lines[lines.length - 1];
|
|
305
|
+
if (lastLine) spinner.message(lastLine);
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
proc.stdout.on('data', handleChunk);
|
|
309
|
+
proc.stderr.on('data', handleChunk);
|
|
310
|
+
|
|
311
|
+
proc.on('close', (code) => {
|
|
312
|
+
if (code === 0) {
|
|
313
|
+
spinner.stop(t(doneKey));
|
|
314
|
+
resolve();
|
|
315
|
+
} else {
|
|
316
|
+
spinner.stop(t(failKey), 2);
|
|
317
|
+
const error = new Error(allOutput || `release script exited with code ${code}`);
|
|
318
|
+
error.buildOutput = allOutput;
|
|
319
|
+
reject(error);
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
proc.on('error', (err) => {
|
|
324
|
+
spinner.stop(t(failKey), 2);
|
|
325
|
+
reject(err);
|
|
326
|
+
});
|
|
327
|
+
});
|
|
259
328
|
}
|
|
260
329
|
|
|
261
330
|
module.exports = {
|
package/lib/utils/checks.js
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
const { exec } = require('node:child_process');
|
|
2
2
|
const { promisify } = require('node:util');
|
|
3
|
-
const readline = require('node:readline');
|
|
4
3
|
const kleur = require('kleur');
|
|
5
|
-
const oraPackage = require('ora');
|
|
6
4
|
const ui = require('./ui');
|
|
7
5
|
const { createTranslator, detectDefaultLanguage } = require('./i18n');
|
|
8
6
|
|
|
9
7
|
const execAsync = promisify(exec);
|
|
10
|
-
const ora = oraPackage.default || oraPackage;
|
|
11
8
|
|
|
12
9
|
// Timeout para verificar se uma ferramenta está instalada (15 s é mais que suficiente)
|
|
13
10
|
const TOOL_CHECK_TIMEOUT = 15_000;
|
|
@@ -19,19 +16,6 @@ const MIN_NODE_VERSION = '18.0.0';
|
|
|
19
16
|
const MIN_FLUTTER_VERSION = '3.24.0';
|
|
20
17
|
const MIN_DART_VERSION = '3.5.0';
|
|
21
18
|
|
|
22
|
-
/**
|
|
23
|
-
* Compare two semver strings. Returns true if actual >= required.
|
|
24
|
-
*/
|
|
25
|
-
function meetsMinVersion(actual, required) {
|
|
26
|
-
if (!actual || !required) return true;
|
|
27
|
-
const parse = (v) => v.replace(/[^0-9.]/g, '').split('.').map(Number);
|
|
28
|
-
const [aMaj, aMin, aPat = 0] = parse(actual);
|
|
29
|
-
const [rMaj, rMin, rPat = 0] = parse(required);
|
|
30
|
-
if (aMaj !== rMaj) return aMaj > rMaj;
|
|
31
|
-
if (aMin !== rMin) return aMin > rMin;
|
|
32
|
-
return aPat >= rPat;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
19
|
const BASE_CHECKS = [
|
|
36
20
|
{
|
|
37
21
|
name: 'Node.js',
|
|
@@ -175,128 +159,43 @@ function extractVersion(stdout, checkName) {
|
|
|
175
159
|
return m ? m[0] : raw.slice(0, 20);
|
|
176
160
|
}
|
|
177
161
|
|
|
178
|
-
/**
|
|
179
|
-
* Wait for the user to press Enter in the terminal.
|
|
180
|
-
* Used to pause the flow so the user can install something or authenticate.
|
|
181
|
-
*/
|
|
182
|
-
function waitForUserInput(prompt) {
|
|
183
|
-
return new Promise((resolve) => {
|
|
184
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
185
|
-
const done = () => { rl.close(); resolve(); };
|
|
186
|
-
rl.question(`\n ${kleur.cyan(prompt)}\n `, done);
|
|
187
|
-
rl.on('close', resolve);
|
|
188
|
-
rl.on('error', resolve);
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
|
|
192
162
|
async function runSingleCheck(check, options = {}) {
|
|
193
163
|
const showVersion = check.showVersion !== undefined ? check.showVersion : (options.showVersion !== undefined ? options.showVersion : true);
|
|
194
|
-
const silent = options.silent === true;
|
|
195
|
-
const t = check.t || createTranslator(check.language || detectDefaultLanguage());
|
|
196
|
-
const spinner = silent ? null : ora(t('checks.checking', { name: check.name })).start();
|
|
197
164
|
let autoInstallFailed = false;
|
|
198
165
|
|
|
199
166
|
try {
|
|
200
167
|
const { stdout } = await execAsync(check.command, { encoding: 'utf8', timeout: TOOL_CHECK_TIMEOUT });
|
|
201
168
|
const version = showVersion ? extractVersion(stdout, check.name) : null;
|
|
202
|
-
const msg = version
|
|
203
|
-
? t('checks.foundWithVersion', { name: check.name, version })
|
|
204
|
-
: t('checks.found', { name: check.name });
|
|
205
|
-
|
|
206
|
-
if (!silent) {
|
|
207
|
-
// Warn if version is below minimum requirement
|
|
208
|
-
if (version && check.minVersion && !meetsMinVersion(version, check.minVersion)) {
|
|
209
|
-
spinner.warn(`${msg} ${kleur.yellow(`(mínimo recomendado: ${check.minVersion})`)}`);
|
|
210
|
-
} else {
|
|
211
|
-
spinner.succeed(msg);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
169
|
return { ...check, ok: true, version: version || null };
|
|
215
170
|
} catch (err) {
|
|
216
171
|
const diagnosis = diagnoseFailure(err);
|
|
217
172
|
if (check.tryInstall) {
|
|
218
|
-
if (!silent) spinner.text = t(check.tryInstallMessageKey || 'setup.flutterfire.installing');
|
|
219
173
|
try {
|
|
220
174
|
await execAsync(check.tryInstall, { encoding: 'utf8', timeout: INSTALL_TIMEOUT });
|
|
221
175
|
const { stdout: retryOut } = await execAsync(check.retryCommand || check.command, { encoding: 'utf8', timeout: TOOL_CHECK_TIMEOUT });
|
|
222
176
|
const version = showVersion ? extractVersion(retryOut, check.name) : null;
|
|
223
|
-
const msg = version
|
|
224
|
-
? t('checks.foundWithVersion', { name: check.name, version: version || 'installed' })
|
|
225
|
-
: t('checks.found', { name: check.name });
|
|
226
|
-
if (!silent) spinner.succeed(msg);
|
|
227
177
|
return { ...check, ok: true, version: version || null };
|
|
228
178
|
} catch {
|
|
229
179
|
autoInstallFailed = true;
|
|
230
180
|
}
|
|
231
181
|
}
|
|
232
|
-
|
|
233
|
-
// Guided interactive prompt: show instructions and wait for the user to act.
|
|
234
|
-
if (check.waitPrompt && !silent) {
|
|
235
|
-
spinner.stop();
|
|
236
|
-
if (check.failHint) {
|
|
237
|
-
console.log(kleur.yellow(`\n ⚠ ${check.name} não encontrado.\n`));
|
|
238
|
-
console.log(kleur.bold(' Execute:\n'));
|
|
239
|
-
console.log(kleur.cyan(` ${check.failHint}\n`));
|
|
240
|
-
}
|
|
241
|
-
await waitForUserInput(check.waitPrompt);
|
|
242
|
-
try {
|
|
243
|
-
const { stdout: retryOut } = await execAsync(check.command, { encoding: 'utf8', timeout: TOOL_CHECK_TIMEOUT });
|
|
244
|
-
const version = showVersion ? extractVersion(retryOut, check.name) : null;
|
|
245
|
-
const msg = version
|
|
246
|
-
? t('checks.foundWithVersion', { name: check.name, version })
|
|
247
|
-
: t('checks.found', { name: check.name });
|
|
248
|
-
spinner.succeed(msg);
|
|
249
|
-
return { ...check, ok: true, version: version || null };
|
|
250
|
-
} catch {
|
|
251
|
-
// Still failing — fall through to report
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
if (!silent) {
|
|
256
|
-
if (check.required) {
|
|
257
|
-
const detail = autoInstallFailed ? ` — ${t('checks.install.failed')}` : '';
|
|
258
|
-
const diagSuffix = diagnosis ? `\n ${kleur.dim(`→ ${t(`checks.diagnostic.${diagnosis}`, { name: check.name })}`)}` : '';
|
|
259
|
-
spinner.fail(t('checks.missing', { name: check.name }) + detail + diagSuffix);
|
|
260
|
-
} else if (diagnosis) {
|
|
261
|
-
spinner.warn(t(`checks.diagnostic.${diagnosis}`, { name: check.name }));
|
|
262
|
-
} else {
|
|
263
|
-
const hint = !check.waitPrompt && check.failHint ? `\n ${kleur.dim(`→ ${check.failHint}`)}` : '';
|
|
264
|
-
if (check.warnMessage) {
|
|
265
|
-
spinner.warn(check.warnMessage + hint);
|
|
266
|
-
} else if (check.warnMessageKey) {
|
|
267
|
-
spinner.warn(t(check.warnMessageKey) + hint);
|
|
268
|
-
} else {
|
|
269
|
-
spinner.warn(t('checks.notFound', { name: check.name }) + hint);
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
182
|
return { ...check, ok: false, autoInstallFailed, diagnosis };
|
|
275
183
|
}
|
|
276
184
|
}
|
|
277
185
|
|
|
278
186
|
async function runChecks(checks, title, options = {}) {
|
|
279
187
|
const t = options.t || createTranslator(options.language || detectDefaultLanguage());
|
|
280
|
-
const { showVersion = true
|
|
281
|
-
|
|
282
|
-
if (!compact) {
|
|
283
|
-
console.log(kleur.bold(`\n${title}`));
|
|
284
|
-
const results = [];
|
|
285
|
-
for (const check of checks) {
|
|
286
|
-
const result = await runSingleCheck({ ...check, t }, { showVersion });
|
|
287
|
-
results.push(result);
|
|
288
|
-
}
|
|
289
|
-
return results;
|
|
290
|
-
}
|
|
188
|
+
const { showVersion = true } = options;
|
|
291
189
|
|
|
292
|
-
//
|
|
190
|
+
// Single spinner over all checks, show failures afterwards. The visual
|
|
191
|
+
// sits inside the clack rail (│) opened by the caller's ui.intro().
|
|
293
192
|
const { spinnerLabel = title, doneLabel = title } = options;
|
|
294
193
|
const spinner = ui.spinner();
|
|
295
194
|
spinner.start(spinnerLabel);
|
|
296
195
|
|
|
297
196
|
const results = [];
|
|
298
197
|
for (const check of checks) {
|
|
299
|
-
results.push(await runSingleCheck({ ...check, t }, { showVersion
|
|
198
|
+
results.push(await runSingleCheck({ ...check, t }, { showVersion }));
|
|
300
199
|
}
|
|
301
200
|
|
|
302
201
|
const failures = results.filter((r) => !r.ok);
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared launcher for `flutter run` invocations.
|
|
3
|
+
*
|
|
4
|
+
* Both `kasy run` and `kasy reset` (reinstall step) call this so the spinner,
|
|
5
|
+
* stage detection, elapsed timer, hot-reload stdin pass-through and SIGINT
|
|
6
|
+
* forwarding stay consistent in one place.
|
|
7
|
+
*
|
|
8
|
+
* Flow:
|
|
9
|
+
* - Spawn flutter with piped stdio.
|
|
10
|
+
* - Show a Clack spinner with elapsed time, ticking every second.
|
|
11
|
+
* - As Flutter logs scroll, update the spinner message when we see a known
|
|
12
|
+
* stage marker (Gradle, Xcode, install, sync, etc.).
|
|
13
|
+
* - When Flutter signals "ready" (DAP key commands), flush buffered output,
|
|
14
|
+
* drop into stdio pass-through and pipe stdin so hot reload works.
|
|
15
|
+
* - On early exit, replay the buffer so the user can see the error.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
const { spawn } = require('node:child_process');
|
|
21
|
+
const kleur = require('kleur');
|
|
22
|
+
const ui = require('./ui');
|
|
23
|
+
|
|
24
|
+
// Markers that tell us the initial build is done and the app is running.
|
|
25
|
+
const FLUTTER_READY_RE = /Flutter run key commands\.|is listening on|VM Service|Dart VM service|To hot reload|Hot restart/i;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Map a chunk of Flutter output to a user-friendly stage label, or null
|
|
29
|
+
* if the chunk doesn't carry any of the known markers.
|
|
30
|
+
*/
|
|
31
|
+
function detectStage(text, t) {
|
|
32
|
+
if (/Running Gradle task 'assembleDebug'/.test(text)) {
|
|
33
|
+
return t('run.stage.gradleFirstTime');
|
|
34
|
+
}
|
|
35
|
+
if (/Running Gradle task/.test(text)) {
|
|
36
|
+
return t('run.stage.gradle');
|
|
37
|
+
}
|
|
38
|
+
if (/Running Xcode build/.test(text)) {
|
|
39
|
+
return t('run.stage.xcode');
|
|
40
|
+
}
|
|
41
|
+
if (/Running pod install/i.test(text)) {
|
|
42
|
+
return t('run.stage.pods');
|
|
43
|
+
}
|
|
44
|
+
if (/Installing build\/.+\.(apk|ipa|app)/i.test(text)) {
|
|
45
|
+
return t('run.stage.installing');
|
|
46
|
+
}
|
|
47
|
+
if (/Syncing files to device/i.test(text)) {
|
|
48
|
+
return t('run.stage.syncing');
|
|
49
|
+
}
|
|
50
|
+
if (/BUILD SUCCESSFUL/.test(text)) {
|
|
51
|
+
return t('run.stage.buildSuccess');
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function formatElapsed(startTime) {
|
|
57
|
+
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
|
58
|
+
if (elapsed < 60) return `${elapsed}s`;
|
|
59
|
+
const m = Math.floor(elapsed / 60);
|
|
60
|
+
const s = elapsed % 60;
|
|
61
|
+
return `${m}m ${s}s`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function render(stageMessage, startTime) {
|
|
65
|
+
return `${stageMessage} ${kleur.dim(`[${formatElapsed(startTime)}]`)}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Spawn `flutter` with the given args inside `projectDir`, surfacing progress
|
|
70
|
+
* through a Clack spinner. Resolves on success, rejects on non-zero exit.
|
|
71
|
+
*/
|
|
72
|
+
function spawnFlutterWithSpinner(args, projectDir, t) {
|
|
73
|
+
return new Promise((resolve, reject) => {
|
|
74
|
+
const proc = spawn('flutter', args, {
|
|
75
|
+
cwd: projectDir,
|
|
76
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const spinner = ui.spinner();
|
|
80
|
+
const startTime = Date.now();
|
|
81
|
+
let stageMessage = t('run.spinner.building');
|
|
82
|
+
let ready = false;
|
|
83
|
+
const buffer = [];
|
|
84
|
+
|
|
85
|
+
spinner.start(render(stageMessage, startTime));
|
|
86
|
+
|
|
87
|
+
// Tick the elapsed time every second while still building. Once ready,
|
|
88
|
+
// the spinner is gone and we stop ticking.
|
|
89
|
+
const tick = setInterval(() => {
|
|
90
|
+
if (!ready) spinner.message(render(stageMessage, startTime));
|
|
91
|
+
}, 1000);
|
|
92
|
+
|
|
93
|
+
const flushAndSwitch = () => {
|
|
94
|
+
if (ready) return;
|
|
95
|
+
ready = true;
|
|
96
|
+
clearInterval(tick);
|
|
97
|
+
const total = formatElapsed(startTime);
|
|
98
|
+
spinner.stop(`${t('run.spinner.ready')} ${kleur.dim(`[${total}]`)}`);
|
|
99
|
+
for (const chunk of buffer) process.stdout.write(chunk);
|
|
100
|
+
buffer.length = 0;
|
|
101
|
+
// Pipe stdin so the user can type r / R / q to control Flutter.
|
|
102
|
+
if (process.stdin.isTTY) {
|
|
103
|
+
try { process.stdin.setRawMode(true); } catch (_) {}
|
|
104
|
+
process.stdin.resume();
|
|
105
|
+
process.stdin.pipe(proc.stdin);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const handleStdout = (chunk) => {
|
|
110
|
+
const text = chunk.toString();
|
|
111
|
+
if (!ready) {
|
|
112
|
+
buffer.push(chunk);
|
|
113
|
+
const nextStage = detectStage(text, t);
|
|
114
|
+
if (nextStage && nextStage !== stageMessage) {
|
|
115
|
+
stageMessage = nextStage;
|
|
116
|
+
spinner.message(render(stageMessage, startTime));
|
|
117
|
+
}
|
|
118
|
+
if (FLUTTER_READY_RE.test(text)) flushAndSwitch();
|
|
119
|
+
} else {
|
|
120
|
+
process.stdout.write(chunk);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const handleStderr = (chunk) => {
|
|
125
|
+
if (!ready) {
|
|
126
|
+
buffer.push(chunk);
|
|
127
|
+
if (FLUTTER_READY_RE.test(chunk.toString())) flushAndSwitch();
|
|
128
|
+
} else {
|
|
129
|
+
process.stderr.write(chunk);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
proc.stdout.on('data', handleStdout);
|
|
134
|
+
proc.stderr.on('data', handleStderr);
|
|
135
|
+
|
|
136
|
+
const sigintHandler = () => { try { proc.kill('SIGINT'); } catch (_) {} };
|
|
137
|
+
process.on('SIGINT', sigintHandler);
|
|
138
|
+
|
|
139
|
+
const cleanup = () => {
|
|
140
|
+
clearInterval(tick);
|
|
141
|
+
process.off('SIGINT', sigintHandler);
|
|
142
|
+
if (process.stdin.isTTY) {
|
|
143
|
+
try { process.stdin.setRawMode(false); } catch (_) {}
|
|
144
|
+
process.stdin.unpipe(proc.stdin);
|
|
145
|
+
process.stdin.pause();
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
proc.on('close', (code) => {
|
|
150
|
+
cleanup();
|
|
151
|
+
if (!ready) {
|
|
152
|
+
spinner.stop(t('run.spinner.failed'), 2);
|
|
153
|
+
for (const chunk of buffer) process.stdout.write(chunk);
|
|
154
|
+
}
|
|
155
|
+
if (code === 0) resolve();
|
|
156
|
+
else reject(new Error(`flutter exited with code ${code}`));
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
proc.on('error', (err) => {
|
|
160
|
+
cleanup();
|
|
161
|
+
if (!ready) spinner.stop(t('run.spinner.failed'), 2);
|
|
162
|
+
reject(err);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
module.exports = {
|
|
168
|
+
spawnFlutterWithSpinner,
|
|
169
|
+
// Exported for tests and reuse in non-flutter spawn helpers.
|
|
170
|
+
detectStage,
|
|
171
|
+
formatElapsed,
|
|
172
|
+
FLUTTER_READY_RE,
|
|
173
|
+
};
|