kasy-cli 1.14.0 → 1.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/bin/kasy.js +18 -5
  2. package/lib/commands/icon.js +29 -1
  3. package/lib/commands/ios.js +8 -2
  4. package/lib/commands/reset.js +100 -2
  5. package/lib/commands/run.js +61 -2
  6. package/lib/commands/splash.js +11 -0
  7. package/lib/scaffold/backends/api/pubspec.yaml.tpl +4 -2
  8. package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +4 -2
  9. package/lib/utils/apple-release.js +30 -0
  10. package/lib/utils/checks.js +41 -2
  11. package/lib/utils/debug.js +75 -0
  12. package/lib/utils/friendly-error.js +91 -0
  13. package/lib/utils/i18n/messages-en.js +977 -0
  14. package/lib/utils/i18n/messages-es.js +975 -0
  15. package/lib/utils/i18n/messages-pt.js +975 -0
  16. package/lib/utils/i18n.js +21 -2818
  17. package/lib/utils/png-padding.js +252 -0
  18. package/package.json +8 -3
  19. package/templates/firebase/android/app/src/main/kotlin/com/aicrus/firebase/kit/MyWidget.kt +12 -11
  20. package/templates/firebase/android/app/src/main/res/drawable-hdpi/android12splash.png +0 -0
  21. package/templates/firebase/android/app/src/main/res/drawable-hdpi/ic_launcher_background.png +0 -0
  22. package/templates/firebase/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png +0 -0
  23. package/templates/firebase/android/app/src/main/res/drawable-mdpi/android12splash.png +0 -0
  24. package/templates/firebase/android/app/src/main/res/drawable-mdpi/ic_launcher_background.png +0 -0
  25. package/templates/firebase/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png +0 -0
  26. package/templates/firebase/android/app/src/main/res/drawable-night-hdpi/android12splash.png +0 -0
  27. package/templates/firebase/android/app/src/main/res/drawable-night-mdpi/android12splash.png +0 -0
  28. package/templates/firebase/android/app/src/main/res/drawable-night-xhdpi/android12splash.png +0 -0
  29. package/templates/firebase/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png +0 -0
  30. package/templates/firebase/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png +0 -0
  31. package/templates/firebase/android/app/src/main/res/drawable-xhdpi/android12splash.png +0 -0
  32. package/templates/firebase/android/app/src/main/res/drawable-xhdpi/ic_launcher_background.png +0 -0
  33. package/templates/firebase/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png +0 -0
  34. package/templates/firebase/android/app/src/main/res/drawable-xxhdpi/android12splash.png +0 -0
  35. package/templates/firebase/android/app/src/main/res/drawable-xxhdpi/ic_launcher_background.png +0 -0
  36. package/templates/firebase/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png +0 -0
  37. package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/android12splash.png +0 -0
  38. package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_background.png +0 -0
  39. package/templates/firebase/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png +0 -0
  40. package/templates/firebase/android/app/src/main/res/layout/widget_preview.xml +18 -11
  41. package/templates/firebase/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +9 -0
  42. package/templates/firebase/assets/images/icon_android.png +0 -0
  43. package/templates/firebase/assets/images/icon_foreground_empty.png +0 -0
  44. package/templates/firebase/assets/images/splash_logo_dark_android12.png +0 -0
  45. package/templates/firebase/assets/images/splash_logo_light_android12.png +0 -0
  46. package/templates/firebase/lib/components/components.dart +1 -0
  47. package/templates/firebase/lib/components/kasy_avatar.dart +88 -57
  48. package/templates/firebase/lib/components/kasy_avatar_presets.dart +116 -74
  49. package/templates/firebase/lib/components/kasy_tabs.dart +431 -0
  50. package/templates/firebase/lib/core/home_widgets/home_widget_mywidget_service.dart +12 -6
  51. package/templates/firebase/lib/features/home/home_components_page.dart +1 -1
  52. package/templates/firebase/lib/features/home/home_components_preview_registry.dart +316 -93
  53. package/templates/firebase/pubspec.yaml +4 -2
  54. package/templates/firebase/web/index.html +9 -0
@@ -0,0 +1,252 @@
1
+ const fsp = require('node:fs/promises');
2
+ const { PNG } = require('pngjs');
3
+
4
+ const ANDROID12_SAFE_RATIO = 0.4;
5
+ const ANDROID_ADAPTIVE_LOGO_RATIO = 0.65;
6
+ const ANDROID_ADAPTIVE_CANVAS = 1024;
7
+
8
+ async function readPng(filePath) {
9
+ const buffer = await fsp.readFile(filePath);
10
+ return new Promise((resolve, reject) => {
11
+ new PNG().parse(buffer, (err, data) => {
12
+ if (err) reject(err);
13
+ else resolve(data);
14
+ });
15
+ });
16
+ }
17
+
18
+ function resizeBilinear(src, dstW, dstH) {
19
+ const dst = new PNG({ width: dstW, height: dstH });
20
+ const srcW = src.width;
21
+ const srcH = src.height;
22
+ const xRatio = srcW > 1 ? (srcW - 1) / dstW : 0;
23
+ const yRatio = srcH > 1 ? (srcH - 1) / dstH : 0;
24
+
25
+ for (let y = 0; y < dstH; y++) {
26
+ const sy = (y + 0.5) * yRatio;
27
+ const y0 = Math.floor(sy);
28
+ const y1 = Math.min(y0 + 1, srcH - 1);
29
+ const wy = sy - y0;
30
+
31
+ for (let x = 0; x < dstW; x++) {
32
+ const sx = (x + 0.5) * xRatio;
33
+ const x0 = Math.floor(sx);
34
+ const x1 = Math.min(x0 + 1, srcW - 1);
35
+ const wx = sx - x0;
36
+
37
+ const i00 = (y0 * srcW + x0) * 4;
38
+ const i10 = (y0 * srcW + x1) * 4;
39
+ const i01 = (y1 * srcW + x0) * 4;
40
+ const i11 = (y1 * srcW + x1) * 4;
41
+
42
+ const dstIdx = (y * dstW + x) * 4;
43
+ for (let c = 0; c < 4; c++) {
44
+ const top = src.data[i00 + c] * (1 - wx) + src.data[i10 + c] * wx;
45
+ const bot = src.data[i01 + c] * (1 - wx) + src.data[i11 + c] * wx;
46
+ dst.data[dstIdx + c] = Math.round(top * (1 - wy) + bot * wy);
47
+ }
48
+ }
49
+ }
50
+ return dst;
51
+ }
52
+
53
+ function compositeOnTransparentSquare(logo, canvasSize) {
54
+ const canvas = new PNG({ width: canvasSize, height: canvasSize });
55
+ canvas.data.fill(0);
56
+
57
+ const offsetX = Math.floor((canvasSize - logo.width) / 2);
58
+ const offsetY = Math.floor((canvasSize - logo.height) / 2);
59
+
60
+ for (let y = 0; y < logo.height; y++) {
61
+ for (let x = 0; x < logo.width; x++) {
62
+ const srcIdx = (y * logo.width + x) * 4;
63
+ const dstIdx = ((y + offsetY) * canvasSize + (x + offsetX)) * 4;
64
+ canvas.data[dstIdx] = logo.data[srcIdx];
65
+ canvas.data[dstIdx + 1] = logo.data[srcIdx + 1];
66
+ canvas.data[dstIdx + 2] = logo.data[srcIdx + 2];
67
+ canvas.data[dstIdx + 3] = logo.data[srcIdx + 3];
68
+ }
69
+ }
70
+ return canvas;
71
+ }
72
+
73
+ async function writePng(png, filePath) {
74
+ return new Promise((resolve, reject) => {
75
+ const chunks = [];
76
+ png.pack()
77
+ .on('data', (chunk) => chunks.push(chunk))
78
+ .on('end', async () => {
79
+ try {
80
+ await fsp.writeFile(filePath, Buffer.concat(chunks));
81
+ resolve();
82
+ } catch (e) {
83
+ reject(e);
84
+ }
85
+ })
86
+ .on('error', reject);
87
+ });
88
+ }
89
+
90
+ /**
91
+ * Read a PNG and write a new one of the same dimensions, with the source
92
+ * logo scaled down to fit inside the Android 12+ splash safe area (centered,
93
+ * transparent padding around it). This is what `windowSplashScreenAnimatedIcon`
94
+ * needs so the OS-applied circular mask doesn't clip the logo edges.
95
+ *
96
+ * @param {string} srcPath
97
+ * @param {string} dstPath
98
+ * @param {number} safeRatio fraction of the canvas the logo should occupy (default 0.6)
99
+ */
100
+ async function writeAndroid12Variant(srcPath, dstPath, safeRatio = ANDROID12_SAFE_RATIO) {
101
+ const src = await readPng(srcPath);
102
+ const canvasSize = Math.max(src.width, src.height);
103
+ const safeSide = Math.round(canvasSize * safeRatio);
104
+
105
+ const aspect = src.width / src.height;
106
+ let logoW;
107
+ let logoH;
108
+ if (aspect >= 1) {
109
+ logoW = safeSide;
110
+ logoH = Math.round(safeSide / aspect);
111
+ } else {
112
+ logoH = safeSide;
113
+ logoW = Math.round(safeSide * aspect);
114
+ }
115
+
116
+ const resized = resizeBilinear(src, logoW, logoH);
117
+ const composited = compositeOnTransparentSquare(resized, canvasSize);
118
+ await writePng(composited, dstPath);
119
+ return { canvasSize, logoW, logoH };
120
+ }
121
+
122
+ function sampleSolidBackground(png) {
123
+ const inset = Math.max(2, Math.floor(Math.min(png.width, png.height) * 0.02));
124
+ const corners = [
125
+ [inset, inset],
126
+ [png.width - inset - 1, inset],
127
+ [inset, png.height - inset - 1],
128
+ [png.width - inset - 1, png.height - inset - 1],
129
+ ];
130
+
131
+ let r = 0;
132
+ let g = 0;
133
+ let b = 0;
134
+ let opaque = 0;
135
+ for (const [x, y] of corners) {
136
+ const idx = (y * png.width + x) * 4;
137
+ const alpha = png.data[idx + 3];
138
+ if (alpha < 250) continue;
139
+ r += png.data[idx];
140
+ g += png.data[idx + 1];
141
+ b += png.data[idx + 2];
142
+ opaque += 1;
143
+ }
144
+
145
+ if (opaque === 0) {
146
+ return { r: 255, g: 255, b: 255, sampled: false };
147
+ }
148
+ return {
149
+ r: Math.round(r / opaque),
150
+ g: Math.round(g / opaque),
151
+ b: Math.round(b / opaque),
152
+ sampled: true,
153
+ };
154
+ }
155
+
156
+ function fillSolidColor(png, color) {
157
+ for (let i = 0; i < png.data.length; i += 4) {
158
+ png.data[i] = color.r;
159
+ png.data[i + 1] = color.g;
160
+ png.data[i + 2] = color.b;
161
+ png.data[i + 3] = 255;
162
+ }
163
+ }
164
+
165
+ function compositeLogoOnBackground(background, logo) {
166
+ const offsetX = Math.floor((background.width - logo.width) / 2);
167
+ const offsetY = Math.floor((background.height - logo.height) / 2);
168
+
169
+ for (let y = 0; y < logo.height; y++) {
170
+ for (let x = 0; x < logo.width; x++) {
171
+ const srcIdx = (y * logo.width + x) * 4;
172
+ const alpha = logo.data[srcIdx + 3] / 255;
173
+ if (alpha === 0) continue;
174
+
175
+ const dstIdx = ((y + offsetY) * background.width + (x + offsetX)) * 4;
176
+ const inv = 1 - alpha;
177
+ background.data[dstIdx] = Math.round(logo.data[srcIdx] * alpha + background.data[dstIdx] * inv);
178
+ background.data[dstIdx + 1] = Math.round(logo.data[srcIdx + 1] * alpha + background.data[dstIdx + 1] * inv);
179
+ background.data[dstIdx + 2] = Math.round(logo.data[srcIdx + 2] * alpha + background.data[dstIdx + 2] * inv);
180
+ background.data[dstIdx + 3] = 255;
181
+ }
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Generate the Android adaptive icon background asset. Reads the user-supplied
187
+ * icon PNG, samples a solid background color from its corners (falling back to
188
+ * white when the image is transparent), and writes a square canvas with the
189
+ * logo scaled down and centered. The result is what `flutter_launcher_icons`
190
+ * passes to `adaptive_icon_background`, fully filling the launcher's circular
191
+ * mask without the default white halo around square icons.
192
+ *
193
+ * @param {string} srcPath source icon PNG path
194
+ * @param {string} dstPath output path for icon_android.png
195
+ * @param {object} [options]
196
+ * @param {number} [options.canvasSize] output square size (default 1024)
197
+ * @param {number} [options.logoRatio] logo fill ratio inside canvas (default 0.65)
198
+ * @param {string} [options.backgroundColor] explicit hex like "#01171f" to override sampling
199
+ */
200
+ async function writeAndroidAdaptiveBackground(srcPath, dstPath, options = {}) {
201
+ const canvasSize = options.canvasSize || ANDROID_ADAPTIVE_CANVAS;
202
+ const logoRatio = options.logoRatio || ANDROID_ADAPTIVE_LOGO_RATIO;
203
+
204
+ const src = await readPng(srcPath);
205
+
206
+ let color;
207
+ if (options.backgroundColor) {
208
+ const hex = options.backgroundColor.replace('#', '');
209
+ color = {
210
+ r: parseInt(hex.slice(0, 2), 16),
211
+ g: parseInt(hex.slice(2, 4), 16),
212
+ b: parseInt(hex.slice(4, 6), 16),
213
+ sampled: false,
214
+ };
215
+ } else {
216
+ color = sampleSolidBackground(src);
217
+ }
218
+
219
+ const canvas = new PNG({ width: canvasSize, height: canvasSize });
220
+ fillSolidColor(canvas, color);
221
+
222
+ const logoSize = Math.round(canvasSize * logoRatio);
223
+ const resized = resizeBilinear(src, logoSize, logoSize);
224
+ compositeLogoOnBackground(canvas, resized);
225
+
226
+ await writePng(canvas, dstPath);
227
+ return { canvasSize, logoSize, color };
228
+ }
229
+
230
+ /**
231
+ * Write a fully transparent square PNG. Used to satisfy
232
+ * `adaptive_icon_foreground` when the entire image already lives in the
233
+ * background layer.
234
+ *
235
+ * @param {string} dstPath
236
+ * @param {number} [size]
237
+ */
238
+ async function writeTransparentSquare(dstPath, size = ANDROID_ADAPTIVE_CANVAS) {
239
+ const png = new PNG({ width: size, height: size });
240
+ png.data.fill(0);
241
+ await writePng(png, dstPath);
242
+ return { size };
243
+ }
244
+
245
+ module.exports = {
246
+ writeAndroid12Variant,
247
+ writeAndroidAdaptiveBackground,
248
+ writeTransparentSquare,
249
+ ANDROID12_SAFE_RATIO,
250
+ ANDROID_ADAPTIVE_LOGO_RATIO,
251
+ ANDROID_ADAPTIVE_CANVAS,
252
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kasy-cli",
3
- "version": "1.14.0",
3
+ "version": "1.16.0",
4
4
  "description": "CLI for scaffolding production-ready Flutter SaaS apps with Firebase, Supabase, or API REST backends.",
5
5
  "bin": {
6
6
  "kasy": "./bin/kasy.js"
@@ -39,10 +39,12 @@
39
39
  "validate": "node ./bin/kasy.js validate --analyze-only",
40
40
  "extract:patch": "node ./scripts/extract_patch.js",
41
41
  "check:firebase": "node ./scripts/check-firebase-template.js",
42
+ "test": "for f in test/*.test.js; do node \"$f\" || exit 1; done",
42
43
  "test:google-ios": "node ./test/google-ios-url-scheme.test.js",
43
44
  "test:apple-release": "node ./test/apple-release.test.js",
44
45
  "test:localize-docs": "node ./test/localize-release-docs.test.js",
45
- "test:i18n-accents": "node ./test/i18n-accents.test.js"
46
+ "test:i18n-accents": "node ./test/i18n-accents.test.js",
47
+ "lint": "eslint bin lib scripts"
46
48
  },
47
49
  "dependencies": {
48
50
  "@clack/prompts": "^1.4.0",
@@ -51,7 +53,10 @@
51
53
  "fs-extra": "^11.2.0",
52
54
  "gradient-string": "^1.2.0",
53
55
  "kleur": "^4.1.5",
54
- "prompts": "^2.4.2",
56
+ "pngjs": "^7.0.0",
55
57
  "yaml": "^2.4.2"
58
+ },
59
+ "devDependencies": {
60
+ "eslint": "^9.39.4"
56
61
  }
57
62
  }
@@ -220,17 +220,18 @@ class MyWidgetWidget : GlanceAppWidget() {
220
220
  return DefaultStrings(greeting, hello)
221
221
  }
222
222
 
223
- /// Builds the same Intent the launcher would fire when the user taps the
224
- /// app icon: ACTION_MAIN + CATEGORY_LAUNCHER. Without these flags, the
225
- /// activity receives an empty Intent and go_router lands on the
226
- /// errorBuilder (the "404 - Page not found" screen).
223
+ /// Builds the exact Intent the system launcher fires when the user taps
224
+ /// the app icon. We must NOT add extra flags here — getLaunchIntentForPackage
225
+ /// already returns `FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_RESET_IF_NEEDED`,
226
+ /// which is the same combo the launcher uses. Adding `CLEAR_TOP` destroys
227
+ /// go_router's navigation stack on warm starts and lands the user on the
228
+ /// errorBuilder ("404 - Page not found").
227
229
  private fun launchAppIntent(context: Context): Intent {
228
- return Intent(context, MainActivity::class.java).apply {
229
- action = Intent.ACTION_MAIN
230
- addCategory(Intent.CATEGORY_LAUNCHER)
231
- flags = Intent.FLAG_ACTIVITY_NEW_TASK or
232
- Intent.FLAG_ACTIVITY_CLEAR_TOP or
233
- Intent.FLAG_ACTIVITY_SINGLE_TOP
234
- }
230
+ return context.packageManager.getLaunchIntentForPackage(context.packageName)
231
+ ?: Intent(context, MainActivity::class.java).apply {
232
+ action = Intent.ACTION_MAIN
233
+ addCategory(Intent.CATEGORY_LAUNCHER)
234
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK
235
+ }
235
236
  }
236
237
  }
@@ -1,19 +1,21 @@
1
1
  <?xml version="1.0" encoding="utf-8"?>
2
- <!-- Static preview shown in the Android widget gallery (Android 12+).
3
- Mirrors the real widget layout so the user can see what they're
4
- adding before they drop it on the home screen. The Glance widget
5
- replaces this once placed. -->
2
+ <!-- Static preview shown in the widget gallery (Android 12+).
3
+ Only uses Views allowed by RemoteViews no <Space>, no
4
+ paddingHorizontal/Vertical, no fontFamily. Anything outside
5
+ the allowlist makes the launcher silently fall back to a gray box. -->
6
6
  <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
7
7
  android:layout_width="match_parent"
8
8
  android:layout_height="match_parent"
9
9
  android:background="@drawable/widget_gradient_bg"
10
10
  android:orientation="vertical"
11
- android:padding="16dp">
11
+ android:paddingLeft="16dp"
12
+ android:paddingRight="16dp"
13
+ android:paddingTop="16dp"
14
+ android:paddingBottom="16dp">
12
15
 
13
16
  <TextView
14
17
  android:layout_width="wrap_content"
15
18
  android:layout_height="wrap_content"
16
- android:fontFamily="sans-serif-medium"
17
19
  android:text="Boa noite"
18
20
  android:textColor="#8CFFFFFF"
19
21
  android:textSize="11sp" />
@@ -24,20 +26,25 @@
24
26
  android:layout_marginTop="4dp"
25
27
  android:text="Olá!"
26
28
  android:textColor="#FFFFFFFF"
27
- android:textSize="24sp"
29
+ android:textSize="22sp"
28
30
  android:textStyle="bold" />
29
31
 
30
- <Space
32
+ <!-- Filler row uses TextView with empty text + weight to push the pill
33
+ to the bottom (Space isn't on the RemoteViews allowlist). -->
34
+ <TextView
31
35
  android:layout_width="match_parent"
32
36
  android:layout_height="0dp"
33
- android:layout_weight="1" />
37
+ android:layout_weight="1"
38
+ android:text="" />
34
39
 
35
40
  <TextView
36
41
  android:layout_width="wrap_content"
37
42
  android:layout_height="wrap_content"
38
43
  android:background="@drawable/widget_pro_pill_bg"
39
- android:paddingHorizontal="10dp"
40
- android:paddingVertical="5dp"
44
+ android:paddingLeft="10dp"
45
+ android:paddingRight="10dp"
46
+ android:paddingTop="5dp"
47
+ android:paddingBottom="5dp"
41
48
  android:text="⭐ PRO"
42
49
  android:textColor="#FFFFD700"
43
50
  android:textSize="11sp"
@@ -0,0 +1,9 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
3
+ <background android:drawable="@drawable/ic_launcher_background"/>
4
+ <foreground>
5
+ <inset
6
+ android:drawable="@drawable/ic_launcher_foreground"
7
+ android:inset="16%" />
8
+ </foreground>
9
+ </adaptive-icon>
@@ -22,6 +22,7 @@ export 'kasy_dialog.dart';
22
22
  export 'kasy_otp_verification_bottom_sheet.dart';
23
23
  export 'kasy_skeleton.dart';
24
24
  export 'kasy_swipe_action.dart';
25
+ export 'kasy_tabs.dart';
25
26
  export 'kasy_text_area.dart';
26
27
  export 'kasy_text_field.dart';
27
28
  export 'kasy_text_field_otp.dart';