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
|
|
8
|
+
npm install -g kasy-cli
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
Or run without installing:
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
npx
|
|
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
|
|
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': '
|
|
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': '
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
37
|
+
function requiresActiveSubscription(argv = []) {
|
|
38
|
+
const command = resolveCommandFromArgv(argv);
|
|
39
|
+
return !!command && UPDATE_COMMANDS.has(command);
|
|
40
|
+
}
|
|
22
41
|
|
|
23
|
-
|
|
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
|
-
|
|
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
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
40
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
53
|
-
|
|
107
|
+
// Validate against server
|
|
108
|
+
process.stdout.write(kleur.cyan(t('license.checking')) + ' ');
|
|
109
|
+
const result = await validateOnServer(key);
|
|
54
110
|
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
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
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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 };
|
package/lib/utils/license.js
CHANGED
package/package.json
CHANGED
|
@@ -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
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
599
|
+
@override
|
|
600
|
+
void dispose() {
|
|
601
|
+
_shadowTimer?.cancel();
|
|
602
|
+
super.dispose();
|
|
603
|
+
}
|
|
602
604
|
|
|
603
|
-
void
|
|
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
|
-
|
|
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
|
|
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
|
|
693
|
+
decoration: _pillDecoration,
|
|
657
694
|
child: Row(
|
|
658
695
|
mainAxisSize: MainAxisSize.min,
|
|
659
696
|
children: [
|