kasy-cli 1.2.1 → 1.3.1

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/bin/kasy.js CHANGED
@@ -516,7 +516,7 @@ async function main() {
516
516
  const argv = process.argv.slice(2);
517
517
  const language = await resolveLanguage(argv);
518
518
  if (shouldRequireLicenseForArgv(argv)) {
519
- await ensureLicenseKey({ language });
519
+ await ensureLicenseKey({ language, argv });
520
520
  }
521
521
  await checkForUpdates();
522
522
  const program = buildProgram(language);
package/lib/utils/i18n.js CHANGED
@@ -104,7 +104,11 @@ 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.',
111
+ 'license.subscriptionExpired': '❌ Your subscription has expired. Updates require an active plan. Renew at kasy.dev.\n Your existing projects still work — only updates are locked.',
108
112
  'prompt.license.enter': '👉 Enter your license key (XXXX-XXXX-XXXX-XXXX)',
109
113
  'prompt.license.invalid': 'Invalid format. Use XXXX-XXXX-XXXX-XXXX.',
110
114
  'prompt.appName.enter': 'Enter the name of your app',
@@ -789,7 +793,11 @@ const MESSAGES = {
789
793
  'license.required': '🔑 Chave de ativação necessária para usar o Kasy CLI',
790
794
  'license.checking': 'Validando chave de ativação...',
791
795
  'license.saved': '✅ Chave validada com sucesso',
792
- 'license.invalid': 'Formato inválido. Use XXXX-XXXX-XXXX-XXXX.',
796
+ 'license.invalid': 'Chave inválida. Verifique sua chave ou entre em contato em kasy.dev.',
797
+ 'license.expired': '❌ Sua assinatura expirou. Renove em kasy.dev.',
798
+ 'license.inactive': '❌ Sua chave foi desativada. Entre em contato em kasy.dev.',
799
+ 'license.offlineWarning': '⚠️ Servidor fora do ar — continuando no modo offline.',
800
+ 'license.subscriptionExpired': '❌ Sua assinatura expirou. Atualizacoes exigem plano ativo. Renove em kasy.dev.\n Seus projetos continuam funcionando — apenas atualizacoes estao bloqueadas.',
793
801
  'prompt.license.enter': '👉 Digite sua chave de ativação (XXXX-XXXX-XXXX-XXXX)',
794
802
  'prompt.license.invalid': 'Formato inválido. Use XXXX-XXXX-XXXX-XXXX.',
795
803
  'prompt.appName.enter': 'Digite o nome do seu app',
@@ -1474,7 +1482,11 @@ const MESSAGES = {
1474
1482
  'license.required': '🔑 Se requiere clave de activación para usar Kasy CLI',
1475
1483
  'license.checking': 'Validando clave de activación...',
1476
1484
  'license.saved': '✅ Clave validada correctamente',
1477
- 'license.invalid': 'Formato inválido. Usa XXXX-XXXX-XXXX-XXXX.',
1485
+ 'license.invalid': 'Clave inválida. Verifica tu clave o contacta soporte en kasy.dev.',
1486
+ 'license.expired': '❌ Tu suscripción ha expirado. Renueva en kasy.dev.',
1487
+ 'license.inactive': '❌ Tu clave fue desactivada. Contacta soporte en kasy.dev.',
1488
+ 'license.offlineWarning': '⚠️ No se pudo conectar al servidor — continuando sin conexión.',
1489
+ 'license.subscriptionExpired': '❌ Tu suscripcion expiró. Las actualizaciones requieren plan activo. Renueva en kasy.dev.\n Tus proyectos siguen funcionando — solo las actualizaciones están bloqueadas.',
1478
1490
  'prompt.license.enter': '👉 Ingresa tu clave de activación (XXXX-XXXX-XXXX-XXXX)',
1479
1491
  'prompt.license.invalid': 'Formato inválido. Usa XXXX-XXXX-XXXX-XXXX.',
1480
1492
  'prompt.appName.enter': 'Ingresa el nombre de tu app',
@@ -1,72 +1,148 @@
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
+ // No key needed
13
+ const FREE_COMMANDS = new Set(['run', 'doctor', 'features', 'version', 'help', 'uninstall']);
14
+
15
+ // Requires key — even if subscription expired (you bought it, it's yours forever)
16
+ const PREMIUM_COMMANDS = new Set(['new', 'setup', 'add', 'remove', 'deploy', 'check', 'validate', 'docs', 'ios', 'codemagic', 'notifications']);
17
+
18
+ // Requires key AND active subscription (update/upgrade only while plan is current)
19
+ const UPDATE_COMMANDS = new Set(['upgrade', 'update']);
20
+
6
21
  function resolveCommandFromArgv(argv = []) {
7
22
  for (let i = 0; i < argv.length; i += 1) {
8
23
  const arg = argv[i];
9
-
10
24
  if (!arg) continue;
25
+ if (arg === '--lang' || arg === '-l') { i += 1; continue; }
26
+ if (arg.startsWith('--') || (arg.startsWith('-') && arg.length > 1)) continue;
27
+ return arg;
28
+ }
29
+ return null;
30
+ }
11
31
 
12
- // Skip global options that take a value.
13
- if (arg === '--lang' || arg === '-l') {
14
- i += 1;
15
- continue;
16
- }
32
+ function shouldRequireLicenseForArgv(argv = []) {
33
+ const command = resolveCommandFromArgv(argv);
34
+ return !!command && (PREMIUM_COMMANDS.has(command) || UPDATE_COMMANDS.has(command));
35
+ }
17
36
 
18
- // Skip long flags with inline value and regular flags.
19
- if (arg.startsWith('--') || (arg.startsWith('-') && arg.length > 1)) {
20
- continue;
21
- }
37
+ function requiresActiveSubscription(argv = []) {
38
+ const command = resolveCommandFromArgv(argv);
39
+ return !!command && UPDATE_COMMANDS.has(command);
40
+ }
22
41
 
23
- return arg;
24
- }
42
+ function validateOnServer(key) {
43
+ return new Promise((resolve) => {
44
+ const body = JSON.stringify({ key });
45
+ const url = new URL(VALIDATE_URL);
46
+ const options = {
47
+ hostname: url.hostname,
48
+ path: url.pathname,
49
+ method: 'POST',
50
+ headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
51
+ timeout: 5000,
52
+ };
53
+ const req = https.request(options, (res) => {
54
+ let data = '';
55
+ res.on('data', (chunk) => { data += chunk; });
56
+ res.on('end', () => {
57
+ try { resolve(JSON.parse(data)); }
58
+ catch { resolve(null); }
59
+ });
60
+ });
61
+ req.on('error', () => resolve(null));
62
+ req.on('timeout', () => { req.destroy(); resolve(null); });
63
+ req.write(body);
64
+ req.end();
65
+ });
66
+ }
25
67
 
26
- return null;
68
+ function getCachedValidation() {
69
+ const config = readLocalConfig();
70
+ const cache = config.licenseCache;
71
+ if (!cache || !cache.checkedAt) return null;
72
+ if (Date.now() - cache.checkedAt > CACHE_TTL_MS) return null;
73
+ return cache;
27
74
  }
28
75
 
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;
76
+ function setCachedValidation(result) {
77
+ const config = readLocalConfig();
78
+ writeLocalConfig({ ...config, licenseCache: { ...result, checkedAt: Date.now() } });
33
79
  }
34
80
 
35
81
  async function ensureLicenseKey(options = {}) {
36
82
  const language = options.language || detectDefaultLanguage();
37
83
  const t = createTranslator(language);
84
+ const needsActiveSubscription = requiresActiveSubscription(options.argv || []);
85
+
86
+ let key = getStoredLicenseKey();
38
87
 
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')));
88
+ // No key stored ask the user
89
+ if (!key || !isLicenseFormatValid(key)) {
44
90
  console.log('');
45
- return stored;
91
+ console.log(kleur.yellow(t('license.required')));
92
+ console.log('');
93
+ const entered = await promptLicenseKey('', { t });
94
+ key = entered.trim().toUpperCase();
95
+ if (!isLicenseFormatValid(key)) throw new Error(t('license.invalid'));
46
96
  }
47
97
 
48
- console.log('');
49
- console.log(kleur.yellow(t('license.required')));
50
- console.log('');
98
+ // Check local cache (avoids network on every command)
99
+ const cached = getCachedValidation();
100
+ if (cached && cached.valid) {
101
+ if (needsActiveSubscription && !cached.subscription_active) {
102
+ throw new Error(t('license.subscriptionExpired'));
103
+ }
104
+ return key;
105
+ }
51
106
 
52
- const entered = await promptLicenseKey(stored || '', { t });
53
- const normalized = entered.trim().toUpperCase();
107
+ // Validate against server
108
+ process.stdout.write(kleur.cyan(t('license.checking')) + ' ');
109
+ const result = await validateOnServer(key);
54
110
 
55
- console.log(kleur.cyan(t('license.checking')));
56
- await new Promise((r) => setTimeout(r, 600));
111
+ // Network error — offline tolerance
112
+ if (!result) {
113
+ console.log(kleur.yellow(t('license.offlineWarning')));
114
+ console.log('');
115
+ return key;
116
+ }
57
117
 
58
- if (!isLicenseFormatValid(normalized)) {
118
+ // Key not found or deactivated
119
+ if (!result.valid) {
120
+ setStoredLicenseKey('');
121
+ setCachedValidation({ valid: false });
122
+ if (result.reason === 'inactive') {
123
+ throw new Error(t('license.inactive'));
124
+ }
59
125
  throw new Error(t('license.invalid'));
60
126
  }
61
127
 
62
- setStoredLicenseKey(normalized);
63
- console.log(kleur.green(t('license.saved')));
128
+ // Key valid — save cache
129
+ setStoredLicenseKey(key);
130
+ setCachedValidation({
131
+ valid: true,
132
+ subscription_active: result.subscription_active,
133
+ plan: result.plan,
134
+ expires_at: result.expires_at,
135
+ });
136
+
137
+ console.log(kleur.green('✓'));
64
138
  console.log('');
65
139
 
66
- return normalized;
140
+ // Check subscription for update commands
141
+ if (needsActiveSubscription && !result.subscription_active) {
142
+ throw new Error(t('license.subscriptionExpired'));
143
+ }
144
+
145
+ return key;
67
146
  }
68
147
 
69
- module.exports = {
70
- ensureLicenseKey,
71
- shouldRequireLicenseForArgv
72
- };
148
+ 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.1",
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: [