kasy-cli 1.2.1 → 1.3.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/README.md CHANGED
@@ -5,13 +5,13 @@ CLI for scaffolding production-ready Flutter SaaS apps.
5
5
  ## Install
6
6
 
7
7
  ```bash
8
- npm install -g @aicrus.io/kasy
8
+ npm install -g kasy-cli
9
9
  ```
10
10
 
11
11
  Or run without installing:
12
12
 
13
13
  ```bash
14
- npx @aicrus.io/kasy new
14
+ npx kasy-cli new
15
15
  ```
16
16
 
17
17
  ## Quick start
package/lib/utils/i18n.js CHANGED
@@ -104,7 +104,10 @@ const MESSAGES = {
104
104
  'license.required': '🔑 License key required to use Kasy CLI',
105
105
  'license.checking': 'Validating license key...',
106
106
  'license.saved': '✅ License validated successfully',
107
- 'license.invalid': 'Invalid license format. Expected XXXX-XXXX-XXXX-XXXX.',
107
+ 'license.invalid': 'Invalid license key. Check your key or contact support at kasy.dev.',
108
+ 'license.expired': '❌ Your license has expired. Renew at kasy.dev.',
109
+ 'license.inactive': '❌ Your license has been deactivated. Contact support at kasy.dev.',
110
+ 'license.offlineWarning': '⚠️ Could not reach license server — continuing offline.',
108
111
  'prompt.license.enter': '👉 Enter your license key (XXXX-XXXX-XXXX-XXXX)',
109
112
  'prompt.license.invalid': 'Invalid format. Use XXXX-XXXX-XXXX-XXXX.',
110
113
  'prompt.appName.enter': 'Enter the name of your app',
@@ -789,7 +792,10 @@ const MESSAGES = {
789
792
  'license.required': '🔑 Chave de ativação necessária para usar o Kasy CLI',
790
793
  'license.checking': 'Validando chave de ativação...',
791
794
  'license.saved': '✅ Chave validada com sucesso',
792
- 'license.invalid': 'Formato inválido. Use XXXX-XXXX-XXXX-XXXX.',
795
+ 'license.invalid': 'Chave inválida. Verifique sua chave ou entre em contato em kasy.dev.',
796
+ 'license.expired': '❌ Sua assinatura expirou. Renove em kasy.dev.',
797
+ 'license.inactive': '❌ Sua chave foi desativada. Entre em contato em kasy.dev.',
798
+ 'license.offlineWarning': '⚠️ Servidor fora do ar — continuando no modo offline.',
793
799
  'prompt.license.enter': '👉 Digite sua chave de ativação (XXXX-XXXX-XXXX-XXXX)',
794
800
  'prompt.license.invalid': 'Formato inválido. Use XXXX-XXXX-XXXX-XXXX.',
795
801
  'prompt.appName.enter': 'Digite o nome do seu app',
@@ -1474,7 +1480,10 @@ const MESSAGES = {
1474
1480
  'license.required': '🔑 Se requiere clave de activación para usar Kasy CLI',
1475
1481
  'license.checking': 'Validando clave de activación...',
1476
1482
  'license.saved': '✅ Clave validada correctamente',
1477
- 'license.invalid': 'Formato inválido. Usa XXXX-XXXX-XXXX-XXXX.',
1483
+ 'license.invalid': 'Clave inválida. Verifica tu clave o contacta soporte en kasy.dev.',
1484
+ 'license.expired': '❌ Tu suscripción ha expirado. Renueva en kasy.dev.',
1485
+ 'license.inactive': '❌ Tu clave fue desactivada. Contacta soporte en kasy.dev.',
1486
+ 'license.offlineWarning': '⚠️ No se pudo conectar al servidor — continuando sin conexión.',
1478
1487
  'prompt.license.enter': '👉 Ingresa tu clave de activación (XXXX-XXXX-XXXX-XXXX)',
1479
1488
  'prompt.license.invalid': 'Formato inválido. Usa XXXX-XXXX-XXXX-XXXX.',
1480
1489
  'prompt.appName.enter': 'Ingresa el nombre de tu app',
@@ -1,72 +1,124 @@
1
+ 'use strict';
2
+
3
+ const https = require('node:https');
1
4
  const kleur = require('kleur');
2
- const { getStoredLicenseKey, isLicenseFormatValid, setStoredLicenseKey } = require('./license');
5
+ const { getStoredLicenseKey, isLicenseFormatValid, setStoredLicenseKey, readLocalConfig, writeLocalConfig } = require('./license');
3
6
  const { promptLicenseKey } = require('./prompts');
4
7
  const { createTranslator, detectDefaultLanguage } = require('./i18n');
5
8
 
9
+ const VALIDATE_URL = 'https://xkefozfsrmqjtesiitvk.supabase.co/functions/v1/validate-license';
10
+ const CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
11
+
12
+ const FREE_COMMANDS = new Set(['run', 'doctor', 'features', 'version', 'help', 'uninstall']);
13
+
6
14
  function resolveCommandFromArgv(argv = []) {
7
15
  for (let i = 0; i < argv.length; i += 1) {
8
16
  const arg = argv[i];
9
-
10
17
  if (!arg) continue;
11
-
12
- // Skip global options that take a value.
13
- if (arg === '--lang' || arg === '-l') {
14
- i += 1;
15
- continue;
16
- }
17
-
18
- // Skip long flags with inline value and regular flags.
19
- if (arg.startsWith('--') || (arg.startsWith('-') && arg.length > 1)) {
20
- continue;
21
- }
22
-
18
+ if (arg === '--lang' || arg === '-l') { i += 1; continue; }
19
+ if (arg.startsWith('--') || (arg.startsWith('-') && arg.length > 1)) continue;
23
20
  return arg;
24
21
  }
25
-
26
22
  return null;
27
23
  }
28
24
 
29
- function shouldRequireLicenseForArgv(_argv = []) {
30
- // License validation is disabled in this public release.
31
- // A future version will validate against a real server before enabling this.
32
- return false;
25
+ function shouldRequireLicenseForArgv(argv = []) {
26
+ const command = resolveCommandFromArgv(argv);
27
+ return !!command && !FREE_COMMANDS.has(command);
28
+ }
29
+
30
+ function validateOnServer(key) {
31
+ return new Promise((resolve) => {
32
+ const body = JSON.stringify({ key });
33
+ const url = new URL(VALIDATE_URL);
34
+ const options = {
35
+ hostname: url.hostname,
36
+ path: url.pathname,
37
+ method: 'POST',
38
+ headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
39
+ timeout: 5000,
40
+ };
41
+ const req = https.request(options, (res) => {
42
+ let data = '';
43
+ res.on('data', (chunk) => { data += chunk; });
44
+ res.on('end', () => {
45
+ try { resolve(JSON.parse(data)); }
46
+ catch { resolve(null); }
47
+ });
48
+ });
49
+ req.on('error', () => resolve(null));
50
+ req.on('timeout', () => { req.destroy(); resolve(null); });
51
+ req.write(body);
52
+ req.end();
53
+ });
54
+ }
55
+
56
+ function getCachedValidation() {
57
+ const config = readLocalConfig();
58
+ const cache = config.licenseCache;
59
+ if (!cache || !cache.checkedAt) return null;
60
+ if (Date.now() - cache.checkedAt > CACHE_TTL_MS) return null;
61
+ return cache;
62
+ }
63
+
64
+ function setCachedValidation(result) {
65
+ const config = readLocalConfig();
66
+ writeLocalConfig({ ...config, licenseCache: { ...result, checkedAt: Date.now() } });
33
67
  }
34
68
 
35
69
  async function ensureLicenseKey(options = {}) {
36
70
  const language = options.language || detectDefaultLanguage();
37
71
  const t = createTranslator(language);
38
72
 
39
- const stored = getStoredLicenseKey();
40
- if (stored && isLicenseFormatValid(stored)) {
41
- console.log(kleur.cyan(t('license.checking')));
42
- await new Promise((r) => setTimeout(r, 600));
43
- console.log(kleur.green(t('license.saved')));
73
+ let key = getStoredLicenseKey();
74
+
75
+ // No key stored — ask the user
76
+ if (!key || !isLicenseFormatValid(key)) {
44
77
  console.log('');
45
- return stored;
78
+ console.log(kleur.yellow(t('license.required')));
79
+ console.log('');
80
+ const entered = await promptLicenseKey('', { t });
81
+ key = entered.trim().toUpperCase();
82
+ if (!isLicenseFormatValid(key)) throw new Error(t('license.invalid'));
46
83
  }
47
84
 
48
- console.log('');
49
- console.log(kleur.yellow(t('license.required')));
50
- console.log('');
85
+ // Check local cache first (avoids network on every command)
86
+ const cached = getCachedValidation();
87
+ if (cached && cached.valid) {
88
+ return key;
89
+ }
51
90
 
52
- const entered = await promptLicenseKey(stored || '', { t });
53
- const normalized = entered.trim().toUpperCase();
91
+ // Validate against server
92
+ process.stdout.write(kleur.cyan(t('license.checking')) + ' ');
93
+ const result = await validateOnServer(key);
54
94
 
55
- console.log(kleur.cyan(t('license.checking')));
56
- await new Promise((r) => setTimeout(r, 600));
95
+ if (!result) {
96
+ // Network error allow if key format is valid (offline tolerance)
97
+ console.log(kleur.yellow(t('license.offlineWarning') || '(offline — using local key)'));
98
+ console.log('');
99
+ return key;
100
+ }
57
101
 
58
- if (!isLicenseFormatValid(normalized)) {
102
+ if (!result.valid) {
103
+ const reason = result.reason;
104
+ if (reason === 'expired') {
105
+ throw new Error(t('license.expired') || 'Your license has expired. Renew at kasy.dev.');
106
+ }
107
+ if (reason === 'inactive') {
108
+ throw new Error(t('license.inactive') || 'Your license has been deactivated. Contact support at kasy.dev.');
109
+ }
110
+ // Key not found — clear stored key so user can re-enter
111
+ setStoredLicenseKey('');
112
+ setCachedValidation({ valid: false });
59
113
  throw new Error(t('license.invalid'));
60
114
  }
61
115
 
62
- setStoredLicenseKey(normalized);
63
- console.log(kleur.green(t('license.saved')));
116
+ // Valid — save key and cache result
117
+ setStoredLicenseKey(key);
118
+ setCachedValidation({ valid: true, plan: result.plan, expires_at: result.expires_at });
119
+ console.log(kleur.green('✓'));
64
120
  console.log('');
65
-
66
- return normalized;
121
+ return key;
67
122
  }
68
123
 
69
- module.exports = {
70
- ensureLicenseKey,
71
- shouldRequireLicenseForArgv
72
- };
124
+ module.exports = { ensureLicenseKey, shouldRequireLicenseForArgv };
@@ -57,6 +57,8 @@ function setStoredLanguage(language) {
57
57
  module.exports = {
58
58
  CONFIG_PATH,
59
59
  isLicenseFormatValid,
60
+ readLocalConfig,
61
+ writeLocalConfig,
60
62
  getStoredLicenseKey,
61
63
  setStoredLicenseKey,
62
64
  getStoredLanguage,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kasy-cli",
3
- "version": "1.2.1",
3
+ "version": "1.3.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"
@@ -554,6 +554,8 @@ class _PreviewControlsState extends State<_PreviewControls> {
554
554
  bool _minimized = true;
555
555
  bool _platformBtnHovered = false;
556
556
  bool _toolsBtnHovered = false;
557
+ bool _shadowVisible = true;
558
+ Timer? _shadowTimer;
557
559
 
558
560
  static const _platformLabels = ['iOS', 'Android', 'iPad'];
559
561
 
@@ -594,13 +596,33 @@ class _PreviewControlsState extends State<_PreviewControls> {
594
596
  });
595
597
  }
596
598
 
597
- void _minimize() => setState(() {
598
- _minimized = true;
599
- _menuOpen = false;
600
- _toolsMenuOpen = false;
601
- });
599
+ @override
600
+ void dispose() {
601
+ _shadowTimer?.cancel();
602
+ super.dispose();
603
+ }
602
604
 
603
- void _maximize() => setState(() => _minimized = false);
605
+ void _hideShadowDuringAnimation() {
606
+ _shadowTimer?.cancel();
607
+ setState(() => _shadowVisible = false);
608
+ _shadowTimer = Timer(const Duration(milliseconds: 140), () {
609
+ if (mounted) setState(() => _shadowVisible = true);
610
+ });
611
+ }
612
+
613
+ void _minimize() {
614
+ setState(() {
615
+ _minimized = true;
616
+ _menuOpen = false;
617
+ _toolsMenuOpen = false;
618
+ });
619
+ _hideShadowDuringAnimation();
620
+ }
621
+
622
+ void _maximize() {
623
+ setState(() => _minimized = false);
624
+ _hideShadowDuringAnimation();
625
+ }
604
626
 
605
627
  @override
606
628
  Widget build(BuildContext context) {
@@ -617,7 +639,22 @@ class _PreviewControlsState extends State<_PreviewControls> {
617
639
  crossAxisAlignment: CrossAxisAlignment.start,
618
640
  mainAxisSize: MainAxisSize.min,
619
641
  children: [
620
- if (_minimized) _buildMinimizedPill() else _buildPill(),
642
+ AnimatedOpacity(
643
+ opacity: _shadowVisible ? 1.0 : 0.0,
644
+ duration: const Duration(milliseconds: 80),
645
+ child: DecoratedBox(
646
+ decoration: const BoxDecoration(
647
+ borderRadius: BorderRadius.all(Radius.circular(24)),
648
+ boxShadow: _pillShadow,
649
+ ),
650
+ child: AnimatedSize(
651
+ duration: const Duration(milliseconds: 140),
652
+ curve: Curves.easeInOutCubic,
653
+ alignment: Alignment.centerRight,
654
+ child: _minimized ? _buildMinimizedPill() : _buildPill(),
655
+ ),
656
+ ),
657
+ ),
621
658
  if (_menuOpen && !_minimized) _buildMenu(),
622
659
  if (_toolsMenuOpen && !_minimized) _buildToolsMenu(),
623
660
  ],
@@ -629,7 +666,7 @@ class _PreviewControlsState extends State<_PreviewControls> {
629
666
  return Container(
630
667
  height: 40,
631
668
  padding: const EdgeInsets.symmetric(horizontal: 4),
632
- decoration: _pillDecoration.copyWith(boxShadow: _pillShadow),
669
+ decoration: _pillDecoration,
633
670
  child: Row(
634
671
  mainAxisSize: MainAxisSize.min,
635
672
  children: [
@@ -653,7 +690,7 @@ class _PreviewControlsState extends State<_PreviewControls> {
653
690
  return Container(
654
691
  height: 40,
655
692
  padding: const EdgeInsets.symmetric(horizontal: 4),
656
- decoration: _pillDecoration.copyWith(boxShadow: _pillShadow),
693
+ decoration: _pillDecoration,
657
694
  child: Row(
658
695
  mainAxisSize: MainAxisSize.min,
659
696
  children: [