kasy-cli 1.13.0 → 1.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/bin/kasy.js +140 -12
  2. package/lib/commands/add.js +2 -2
  3. package/lib/commands/codemagic.js +11 -4
  4. package/lib/commands/deploy.js +3 -3
  5. package/lib/commands/favicon.js +115 -0
  6. package/lib/commands/icon.js +143 -0
  7. package/lib/commands/ios.js +28 -7
  8. package/lib/commands/new.js +8 -20
  9. package/lib/commands/remove.js +1 -1
  10. package/lib/commands/reset.js +385 -0
  11. package/lib/commands/run.js +24 -17
  12. package/lib/commands/splash.js +14 -4
  13. package/lib/commands/update.js +1 -1
  14. package/lib/scaffold/backends/api/patch/README.md +1 -1
  15. package/lib/scaffold/backends/api/patch/android/app/src/main/AndroidManifest.xml +1 -1
  16. package/lib/scaffold/backends/api/patch/lib/features/notifications/api/device_api.dart +53 -0
  17. package/lib/scaffold/backends/api/pubspec.yaml.tpl +11 -1
  18. package/lib/scaffold/backends/firebase/tokens.js +2 -2
  19. package/lib/scaffold/backends/supabase/edge-functions/send-push-notification/index.ts +8 -2
  20. package/lib/scaffold/backends/supabase/migrations/20240101000011_dedupe_device_tokens.sql +34 -0
  21. package/lib/scaffold/backends/supabase/patch/README.md +1 -1
  22. package/lib/scaffold/backends/supabase/patch/android/app/src/main/AndroidManifest.xml +1 -1
  23. package/lib/scaffold/backends/supabase/patch/lib/features/notifications/api/device_api.dart +43 -0
  24. package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +11 -1
  25. package/lib/utils/apple-release.js +115 -16
  26. package/lib/utils/checks.js +45 -107
  27. package/lib/utils/debug.js +75 -0
  28. package/lib/utils/flutter-run.js +173 -0
  29. package/lib/utils/friendly-error.js +91 -0
  30. package/lib/utils/i18n/messages-en.js +970 -0
  31. package/lib/utils/i18n/messages-es.js +968 -0
  32. package/lib/utils/i18n/messages-pt.js +968 -0
  33. package/lib/utils/i18n.js +21 -2483
  34. package/lib/utils/mobile-identity.js +35 -0
  35. package/lib/utils/png-padding.js +120 -0
  36. package/lib/utils/ui.js +114 -0
  37. package/package.json +8 -4
  38. package/templates/firebase/README.en.md +1 -1
  39. package/templates/firebase/README.es.md +1 -1
  40. package/templates/firebase/README.md +1 -1
  41. package/templates/firebase/android/app/build.gradle.kts +10 -1
  42. package/templates/firebase/android/app/src/main/AndroidManifest.xml +1 -1
  43. package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/MainActivity.kt +25 -1
  44. package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/MyWidget.kt +161 -11
  45. package/templates/firebase/android/app/src/main/res/drawable/widget_add_button.xml +15 -0
  46. package/templates/firebase/android/app/src/main/res/drawable/widget_gradient_bg.xml +9 -0
  47. package/templates/firebase/android/app/src/main/res/drawable/widget_gradient_inner.xml +12 -0
  48. package/templates/firebase/android/app/src/main/res/drawable/widget_plan_pill_bg.xml +5 -0
  49. package/templates/firebase/android/app/src/main/res/drawable/widget_preview_image.xml +17 -0
  50. package/templates/firebase/android/app/src/main/res/drawable/widget_pro_pill_bg.xml +5 -0
  51. package/templates/firebase/android/app/src/main/res/drawable-hdpi/android12splash.png +0 -0
  52. package/templates/firebase/android/app/src/main/res/drawable-hdpi/ic_launcher_background.png +0 -0
  53. package/templates/firebase/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png +0 -0
  54. package/templates/firebase/android/app/src/main/res/drawable-mdpi/android12splash.png +0 -0
  55. package/templates/firebase/android/app/src/main/res/drawable-mdpi/ic_launcher_background.png +0 -0
  56. package/templates/firebase/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png +0 -0
  57. package/templates/firebase/android/app/src/main/res/drawable-night-hdpi/android12splash.png +0 -0
  58. package/templates/firebase/android/app/src/main/res/drawable-night-mdpi/android12splash.png +0 -0
  59. package/templates/firebase/android/app/src/main/res/drawable-night-xhdpi/android12splash.png +0 -0
  60. package/templates/firebase/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png +0 -0
  61. package/templates/firebase/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png +0 -0
  62. package/templates/firebase/android/app/src/main/res/drawable-xhdpi/android12splash.png +0 -0
  63. package/templates/firebase/android/app/src/main/res/drawable-xhdpi/ic_launcher_background.png +0 -0
  64. package/templates/firebase/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png +0 -0
  65. package/templates/firebase/android/app/src/main/res/drawable-xxhdpi/android12splash.png +0 -0
  66. package/templates/firebase/android/app/src/main/res/drawable-xxhdpi/ic_launcher_background.png +0 -0
  67. package/templates/firebase/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png +0 -0
  68. package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/android12splash.png +0 -0
  69. package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_background.png +0 -0
  70. package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png +0 -0
  71. package/templates/firebase/android/app/src/main/res/layout/widget_loading.xml +8 -0
  72. package/templates/firebase/android/app/src/main/res/layout/widget_preview.xml +53 -0
  73. package/templates/firebase/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +9 -0
  74. package/templates/firebase/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
  75. package/templates/firebase/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
  76. package/templates/firebase/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
  77. package/templates/firebase/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  78. package/templates/firebase/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  79. package/templates/firebase/android/app/src/main/res/xml/mywidget_info.xml +9 -3
  80. package/templates/firebase/assets/images/favicon.png +0 -0
  81. package/templates/firebase/assets/images/icon.png +0 -0
  82. package/templates/firebase/assets/images/icon_android.png +0 -0
  83. package/templates/firebase/assets/images/icon_foreground_empty.png +0 -0
  84. package/templates/firebase/assets/images/splash_logo_dark_android12.png +0 -0
  85. package/templates/firebase/assets/images/splash_logo_light_android12.png +0 -0
  86. package/templates/firebase/firestore.indexes.json +10 -0
  87. package/templates/firebase/functions/src/core/data/entities/user_device_entity.ts +3 -0
  88. package/templates/firebase/functions/src/core/data/repositories/user_device_repository.ts +17 -1
  89. package/templates/firebase/functions/src/index.ts +1 -0
  90. package/templates/firebase/functions/src/notifications/device_triggers.ts +58 -0
  91. package/templates/firebase/ios/HomeWidgetExtension/MyWidget.swift +116 -33
  92. package/templates/firebase/ios/Runner/AppDelegate.swift +17 -1
  93. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png +0 -0
  94. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png +0 -0
  95. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png +0 -0
  96. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png +0 -0
  97. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png +0 -0
  98. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png +0 -0
  99. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png +0 -0
  100. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png +0 -0
  101. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png +0 -0
  102. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png +0 -0
  103. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png +0 -0
  104. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png +0 -0
  105. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png +0 -0
  106. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png +0 -0
  107. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png +0 -0
  108. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png +0 -0
  109. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png +0 -0
  110. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png +0 -0
  111. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png +0 -0
  112. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png +0 -0
  113. package/templates/firebase/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png +0 -0
  114. package/templates/firebase/ios/Runner/Info.plist +2 -2
  115. package/templates/firebase/ios/Runner/es.lproj/InfoPlist.strings +1 -1
  116. package/templates/firebase/ios/Runner/pt-BR.lproj/InfoPlist.strings +1 -1
  117. package/templates/firebase/ios/Runner/pt.lproj/InfoPlist.strings +1 -1
  118. package/templates/firebase/lib/components/components.dart +1 -0
  119. package/templates/firebase/lib/components/kasy_avatar.dart +88 -57
  120. package/templates/firebase/lib/components/kasy_avatar_presets.dart +116 -74
  121. package/templates/firebase/lib/components/kasy_button.dart +8 -0
  122. package/templates/firebase/lib/components/kasy_tabs.dart +431 -0
  123. package/templates/firebase/lib/core/bottom_menu/kasy_bottom_bar_factory.dart +18 -0
  124. package/templates/firebase/lib/core/home_widgets/home_widget_mywidget_service.dart +73 -19
  125. package/templates/firebase/lib/core/home_widgets/home_widget_service.dart +22 -8
  126. package/templates/firebase/lib/core/shared_preferences/shared_preferences.dart +16 -4
  127. package/templates/firebase/lib/core/states/components/maybeshow_component.dart +4 -8
  128. package/templates/firebase/lib/core/states/user_state_notifier.dart +13 -1
  129. package/templates/firebase/lib/features/home/home_components_page.dart +1 -1
  130. package/templates/firebase/lib/features/home/home_components_preview_registry.dart +316 -93
  131. package/templates/firebase/lib/features/home/home_page.dart +0 -6
  132. package/templates/firebase/lib/features/notifications/api/device_api.dart +57 -0
  133. package/templates/firebase/lib/features/notifications/providers/models/notification.dart +11 -1
  134. package/templates/firebase/lib/features/notifications/repositories/device_repository.dart +9 -0
  135. package/templates/firebase/lib/features/notifications/repositories/notifications_repository.dart +1 -4
  136. package/templates/firebase/lib/features/notifications/shared/att_permission.dart +28 -8
  137. package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +16 -1
  138. package/templates/firebase/lib/features/notifications/ui/widgets/empty_notifications.dart +44 -11
  139. package/templates/firebase/lib/features/settings/ui/components/admin/admin_bottom_sheet.dart +31 -29
  140. package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +21 -5
  141. package/templates/firebase/lib/i18n/en.i18n.json +4 -1
  142. package/templates/firebase/lib/i18n/es.i18n.json +4 -1
  143. package/templates/firebase/lib/i18n/pt.i18n.json +4 -1
  144. package/templates/firebase/pubspec.yaml +10 -3
  145. package/templates/firebase/test/features/notifications/data/device_api_fake.dart +9 -0
  146. package/templates/firebase/web/favicon.png +0 -0
  147. package/templates/firebase/web/icons/Icon-192.png +0 -0
  148. package/templates/firebase/web/icons/Icon-512.png +0 -0
  149. package/templates/firebase/web/icons/Icon-maskable-192.png +0 -0
  150. package/templates/firebase/web/icons/Icon-maskable-512.png +0 -0
  151. package/templates/firebase/web/index.html +9 -0
  152. package/templates/firebase/web/manifest.json +3 -3
  153. package/templates/firebase/assets/images/app_icon.png +0 -0
  154. package/templates/firebase/assets/images/onboarding/img1.jpg +0 -0
  155. package/templates/firebase/assets/images/onboarding/onboarding.png +0 -0
  156. package/templates/firebase/lib/core/states/components/maybe_ask_biometric_setup.dart +0 -88
  157. package/templates/firebase/lib/features/notifications/shared/notification_permission_bottom_sheet.dart +0 -144
@@ -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,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
+ };
@@ -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 };