kasy-cli 1.5.2 → 1.5.3
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/lib/commands/new.js +150 -159
- package/lib/commands/run.js +2 -1
- package/lib/utils/checks.js +22 -4
- package/lib/utils/i18n.js +21 -3
- package/lib/utils/ui.js +82 -0
- package/lib/utils/updates.js +11 -13
- package/package.json +2 -1
package/lib/commands/new.js
CHANGED
|
@@ -69,6 +69,7 @@ function openUrl(url) {
|
|
|
69
69
|
} catch (_) {}
|
|
70
70
|
}
|
|
71
71
|
const prompts = require('prompts');
|
|
72
|
+
const ui = require('../utils/ui');
|
|
72
73
|
const fs = require('fs-extra');
|
|
73
74
|
const { createTranslator } = require('../utils/i18n');
|
|
74
75
|
const { getStoredLanguage, setStoredLanguage } = require('../utils/license');
|
|
@@ -534,6 +535,8 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
534
535
|
|
|
535
536
|
printBanner(tr);
|
|
536
537
|
|
|
538
|
+
ui.intro(kleur.bold(tr('new.subtitle2')));
|
|
539
|
+
|
|
537
540
|
// Whether an explicit target directory was provided by the user
|
|
538
541
|
const hasExplicitDir = directory && directory !== '.';
|
|
539
542
|
|
|
@@ -553,21 +556,16 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
553
556
|
// ── 3. Backend selection (support pre-selection via --backend flag) ─────
|
|
554
557
|
let backend = normalizeBackend(backendHint);
|
|
555
558
|
if (!backend) {
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
initial: 0,
|
|
567
|
-
},
|
|
568
|
-
{ onCancel: cancel }
|
|
569
|
-
);
|
|
570
|
-
backend = selectedBackend;
|
|
559
|
+
backend = await ui.select({
|
|
560
|
+
message: tr('new.q.backend'),
|
|
561
|
+
initialValue: 'firebase',
|
|
562
|
+
options: [
|
|
563
|
+
{ value: 'firebase', label: '🔥 Firebase', hint: tr('new.q.backend.firebase.desc') },
|
|
564
|
+
{ value: 'supabase', label: '🟢 Supabase', hint: tr('new.q.backend.supabase.desc') },
|
|
565
|
+
{ value: 'api', label: '🔗 API REST', hint: tr('new.q.backend.api.desc') },
|
|
566
|
+
],
|
|
567
|
+
onCancel: cancel,
|
|
568
|
+
});
|
|
571
569
|
} else {
|
|
572
570
|
const backendLabel = { firebase: '🔥 Firebase', supabase: '🟢 Supabase', api: '🔗 API REST' }[backend] || backend;
|
|
573
571
|
console.log(kleur.gray(` Backend: ${kleur.white(backendLabel)}`));
|
|
@@ -601,41 +599,97 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
601
599
|
}
|
|
602
600
|
}
|
|
603
601
|
|
|
602
|
+
// ── App identity: name + bundle ID — first things first ────────────────────
|
|
603
|
+
let core;
|
|
604
|
+
if (yes) {
|
|
605
|
+
if (!hasExplicitDir) {
|
|
606
|
+
console.error(kleur.red(`\n ✗ --yes requires an app name: kasy new MyApp --yes\n`));
|
|
607
|
+
process.exit(1);
|
|
608
|
+
}
|
|
609
|
+
const appName = path.basename(targetDir);
|
|
610
|
+
const slug = appName
|
|
611
|
+
.normalize('NFD')
|
|
612
|
+
.replace(/[̀-ͯ]/g, '')
|
|
613
|
+
.toLowerCase()
|
|
614
|
+
.replace(/[^a-z0-9]/g, '');
|
|
615
|
+
const bundleId = (slug && !/^\d/.test(slug)) ? `com.${slug}.app` : 'com.example.app';
|
|
616
|
+
core = { appName, bundleId };
|
|
617
|
+
console.log(kleur.gray(` App: ${kleur.white(appName)}`));
|
|
618
|
+
console.log(kleur.gray(` Bundle: ${kleur.white(bundleId)}`));
|
|
619
|
+
} else {
|
|
620
|
+
const appName = await ui.text({
|
|
621
|
+
message: tr('new.firebase.q.appName'),
|
|
622
|
+
placeholder: tr('new.firebase.q.appName.hint'),
|
|
623
|
+
initialValue: hasExplicitDir ? path.basename(targetDir) : '',
|
|
624
|
+
validate: (v) => (v && v.trim() ? undefined : tr('new.firebase.q.appName.required')),
|
|
625
|
+
onCancel: cancel,
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
const defaultBundleId = (() => {
|
|
629
|
+
const name = (appName || '').trim();
|
|
630
|
+
if (!name) return 'com.example.app';
|
|
631
|
+
const slug = name
|
|
632
|
+
.normalize('NFD')
|
|
633
|
+
.replace(/[̀-ͯ]/g, '')
|
|
634
|
+
.toLowerCase()
|
|
635
|
+
.replace(/[^a-z0-9]/g, '');
|
|
636
|
+
if (!slug || /^\d/.test(slug)) return 'com.example.app';
|
|
637
|
+
return `com.${slug}.app`;
|
|
638
|
+
})();
|
|
639
|
+
|
|
640
|
+
const bundleId = await ui.text({
|
|
641
|
+
message: tr('new.firebase.q.bundleId'),
|
|
642
|
+
placeholder: tr('new.firebase.q.bundleId.hint'),
|
|
643
|
+
initialValue: defaultBundleId,
|
|
644
|
+
validate: (v) => {
|
|
645
|
+
if (!v || !v.trim()) return tr('new.firebase.q.bundleId.required');
|
|
646
|
+
return /^[a-zA-Z][\w]*(\.[a-zA-Z][\w]*)+$/.test(v.trim())
|
|
647
|
+
? undefined
|
|
648
|
+
: tr('new.firebase.q.bundleId.invalid');
|
|
649
|
+
},
|
|
650
|
+
onCancel: cancel,
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
core = { appName, bundleId };
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// Resolve targetDir now that we have the app name
|
|
657
|
+
if (!targetDir) {
|
|
658
|
+
const folderName = toPackageName(core.appName.trim());
|
|
659
|
+
targetDir = path.resolve(process.cwd(), folderName);
|
|
660
|
+
if (await fs.pathExists(targetDir)) {
|
|
661
|
+
const contents = await fs.readdir(targetDir);
|
|
662
|
+
if (contents.length > 0) {
|
|
663
|
+
throw new Error(tr('new.firebase.error.dirNotEmpty', { path: targetDir }));
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
604
668
|
// ── Firebase setup mode (create vs existing) ────────────────────────────────
|
|
605
669
|
// Firebase backend: full setup. Supabase/API: Firebase only for push notifications (FCM).
|
|
606
670
|
let firebaseSetupMode = 'existing';
|
|
607
671
|
if (!yes) {
|
|
608
672
|
if (backend === 'firebase') {
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
initial: 0,
|
|
619
|
-
},
|
|
620
|
-
{ onCancel: cancel }
|
|
621
|
-
);
|
|
622
|
-
firebaseSetupMode = setupMode;
|
|
673
|
+
firebaseSetupMode = await ui.select({
|
|
674
|
+
message: tr('new.firebase.q.setupMode'),
|
|
675
|
+
initialValue: 'create',
|
|
676
|
+
options: [
|
|
677
|
+
{ value: 'create', label: tr('new.firebase.q.setupMode.create') },
|
|
678
|
+
{ value: 'existing', label: tr('new.firebase.q.setupMode.existing') },
|
|
679
|
+
],
|
|
680
|
+
onCancel: cancel,
|
|
681
|
+
});
|
|
623
682
|
} else if (backend === 'supabase' || backend === 'api') {
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
initial: 0,
|
|
635
|
-
},
|
|
636
|
-
{ onCancel: cancel }
|
|
637
|
-
);
|
|
638
|
-
firebaseSetupMode = setupMode;
|
|
683
|
+
ui.note(tr('new.firebase.q.setupMode.push.explain'));
|
|
684
|
+
firebaseSetupMode = await ui.select({
|
|
685
|
+
message: tr('new.firebase.q.setupMode.push'),
|
|
686
|
+
initialValue: 'create',
|
|
687
|
+
options: [
|
|
688
|
+
{ value: 'create', label: tr('new.firebase.q.setupMode.create') },
|
|
689
|
+
{ value: 'existing', label: tr('new.firebase.q.setupMode.existing') },
|
|
690
|
+
],
|
|
691
|
+
onCancel: cancel,
|
|
692
|
+
});
|
|
639
693
|
}
|
|
640
694
|
}
|
|
641
695
|
|
|
@@ -643,25 +697,62 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
643
697
|
// Asked after app name + setup context are clear, so the user understands what it controls.
|
|
644
698
|
let isQuick = yes; // --yes implies Quick mode
|
|
645
699
|
if (!yes) {
|
|
646
|
-
const
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
initial: 0,
|
|
656
|
-
},
|
|
657
|
-
{ onCancel: cancel }
|
|
658
|
-
);
|
|
700
|
+
const wizardMode = await ui.select({
|
|
701
|
+
message: tr('new.q.mode'),
|
|
702
|
+
initialValue: 'quick',
|
|
703
|
+
options: [
|
|
704
|
+
{ value: 'quick', label: tr('new.q.mode.quick') },
|
|
705
|
+
{ value: 'advanced', label: tr('new.q.mode.advanced') },
|
|
706
|
+
],
|
|
707
|
+
onCancel: cancel,
|
|
708
|
+
});
|
|
659
709
|
isQuick = wizardMode === 'quick';
|
|
660
710
|
}
|
|
661
711
|
|
|
662
712
|
// ── Backend-specific prerequisites (dynamic: only show what's not yet verified) ─
|
|
663
713
|
printPrerequisites(tr, backend, firebaseSetupMode, backendCheckResults);
|
|
664
714
|
|
|
715
|
+
// ── Firebase project ID (if using an existing project) ──────────────────────
|
|
716
|
+
if (!yes) {
|
|
717
|
+
const needFirebaseProjectId =
|
|
718
|
+
(backend === 'firebase' && firebaseSetupMode === 'existing') ||
|
|
719
|
+
((backend === 'supabase' || backend === 'api') && firebaseSetupMode === 'existing');
|
|
720
|
+
if (needFirebaseProjectId) {
|
|
721
|
+
const firebaseProjectId = await ui.text({
|
|
722
|
+
message: tr('new.firebase.q.projectId'),
|
|
723
|
+
placeholder: tr('new.firebase.q.projectId.hint') + (backend !== 'firebase' ? ' (FCM + Remote Config)' : ''),
|
|
724
|
+
validate: (v) => {
|
|
725
|
+
if (!v || !v.trim()) return tr('new.firebase.q.projectId.required');
|
|
726
|
+
const id = v.trim();
|
|
727
|
+
if (id.length < 6 || id.length > 30) return 'ID deve ter entre 6 e 30 caracteres (ex: meu-app-123)';
|
|
728
|
+
if (!/^[a-z][a-z0-9-]*[a-z0-9]$/.test(id)) return 'ID inválido: use letras minúsculas, números e hífens, começando com letra (ex: meu-app-123)';
|
|
729
|
+
return undefined;
|
|
730
|
+
},
|
|
731
|
+
onCancel: cancel,
|
|
732
|
+
});
|
|
733
|
+
core.firebaseProjectId = firebaseProjectId;
|
|
734
|
+
}
|
|
735
|
+
} else {
|
|
736
|
+
let firebaseProjectId = projectHint?.trim() || '';
|
|
737
|
+
if (!firebaseProjectId && backend === 'firebase') {
|
|
738
|
+
const { pid } = await prompts(
|
|
739
|
+
{
|
|
740
|
+
type: 'text',
|
|
741
|
+
name: 'pid',
|
|
742
|
+
message: tr('new.firebase.q.projectId'),
|
|
743
|
+
hint: tr('new.firebase.q.projectId.hint'),
|
|
744
|
+
validate: (v) => (v && v.trim() ? true : tr('new.firebase.q.projectId.required')),
|
|
745
|
+
},
|
|
746
|
+
{ onCancel: cancel }
|
|
747
|
+
);
|
|
748
|
+
firebaseProjectId = pid?.trim() || '';
|
|
749
|
+
}
|
|
750
|
+
if (firebaseProjectId) {
|
|
751
|
+
core.firebaseProjectId = firebaseProjectId;
|
|
752
|
+
console.log(kleur.gray(` Project: ${kleur.white(firebaseProjectId)}`));
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
665
756
|
// ── Firebase region — Quick mode uses default (us-central1) ──────────
|
|
666
757
|
let firebaseRegion = 'us-central1';
|
|
667
758
|
if (backend === 'firebase' && !isQuick) {
|
|
@@ -682,93 +773,6 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
682
773
|
firebaseRegion = region || 'us-central1';
|
|
683
774
|
}
|
|
684
775
|
|
|
685
|
-
// ── Core questions (appName, bundleId) ────────────────────────────────────
|
|
686
|
-
const coreQuestions = [
|
|
687
|
-
{
|
|
688
|
-
type: 'text',
|
|
689
|
-
name: 'appName',
|
|
690
|
-
message: tr('new.firebase.q.appName'),
|
|
691
|
-
hint: tr('new.firebase.q.appName.hint'),
|
|
692
|
-
initial: hasExplicitDir ? path.basename(targetDir) : '',
|
|
693
|
-
validate: (v) => (v && v.trim() ? true : tr('new.firebase.q.appName.required')),
|
|
694
|
-
},
|
|
695
|
-
{
|
|
696
|
-
type: 'text',
|
|
697
|
-
name: 'bundleId',
|
|
698
|
-
message: tr('new.firebase.q.bundleId'),
|
|
699
|
-
hint: tr('new.firebase.q.bundleId.hint'),
|
|
700
|
-
initial: (prev, values) => {
|
|
701
|
-
const name = (values?.appName || prev || '').trim();
|
|
702
|
-
if (!name) return 'com.example.app';
|
|
703
|
-
const slug = name
|
|
704
|
-
.normalize('NFD')
|
|
705
|
-
.replace(/[\u0300-\u036f]/g, '')
|
|
706
|
-
.toLowerCase()
|
|
707
|
-
.replace(/[^a-z0-9]/g, '');
|
|
708
|
-
if (!slug || /^\d/.test(slug)) return 'com.example.app';
|
|
709
|
-
return `com.${slug}.app`;
|
|
710
|
-
},
|
|
711
|
-
validate: (v) => {
|
|
712
|
-
if (!v || !v.trim()) return tr('new.firebase.q.bundleId.required');
|
|
713
|
-
return /^[a-zA-Z][\w]*(\.[a-zA-Z][\w]*)+$/.test(v.trim())
|
|
714
|
-
? true
|
|
715
|
-
: tr('new.firebase.q.bundleId.invalid');
|
|
716
|
-
},
|
|
717
|
-
},
|
|
718
|
-
];
|
|
719
|
-
const needFirebaseProjectIdNow =
|
|
720
|
-
(backend === 'firebase' && firebaseSetupMode === 'existing') ||
|
|
721
|
-
((backend === 'supabase' || backend === 'api') && firebaseSetupMode === 'existing');
|
|
722
|
-
if (needFirebaseProjectIdNow) {
|
|
723
|
-
coreQuestions.push({
|
|
724
|
-
type: 'text',
|
|
725
|
-
name: 'firebaseProjectId',
|
|
726
|
-
message: tr('new.firebase.q.projectId'),
|
|
727
|
-
hint: tr('new.firebase.q.projectId.hint') + (backend !== 'firebase' ? ' (FCM + Remote Config)' : ''),
|
|
728
|
-
validate: (v) => {
|
|
729
|
-
if (!v || !v.trim()) return tr('new.firebase.q.projectId.required');
|
|
730
|
-
const id = v.trim();
|
|
731
|
-
if (id.length < 6 || id.length > 30) return 'ID deve ter entre 6 e 30 caracteres (ex: meu-app-123)';
|
|
732
|
-
if (!/^[a-z][a-z0-9-]*[a-z0-9]$/.test(id)) return 'ID inválido: use letras minúsculas, números e hífens, começando com letra (ex: meu-app-123)';
|
|
733
|
-
return true;
|
|
734
|
-
},
|
|
735
|
-
});
|
|
736
|
-
}
|
|
737
|
-
let core;
|
|
738
|
-
if (yes) {
|
|
739
|
-
if (!hasExplicitDir) {
|
|
740
|
-
console.error(kleur.red(`\n ✗ --yes requires an app name: kasy new MyApp --yes\n`));
|
|
741
|
-
process.exit(1);
|
|
742
|
-
}
|
|
743
|
-
const appName = path.basename(targetDir);
|
|
744
|
-
const slug = appName
|
|
745
|
-
.normalize('NFD')
|
|
746
|
-
.replace(/[\u0300-\u036f]/g, '')
|
|
747
|
-
.toLowerCase()
|
|
748
|
-
.replace(/[^a-z0-9]/g, '');
|
|
749
|
-
const bundleId = (slug && !/^\d/.test(slug)) ? `com.${slug}.app` : 'com.example.app';
|
|
750
|
-
let firebaseProjectId = projectHint?.trim() || '';
|
|
751
|
-
if (!firebaseProjectId && backend === 'firebase') {
|
|
752
|
-
const { pid } = await prompts(
|
|
753
|
-
{
|
|
754
|
-
type: 'text',
|
|
755
|
-
name: 'pid',
|
|
756
|
-
message: tr('new.firebase.q.projectId'),
|
|
757
|
-
hint: tr('new.firebase.q.projectId.hint'),
|
|
758
|
-
validate: (v) => (v && v.trim() ? true : tr('new.firebase.q.projectId.required')),
|
|
759
|
-
},
|
|
760
|
-
{ onCancel: cancel }
|
|
761
|
-
);
|
|
762
|
-
firebaseProjectId = pid?.trim() || '';
|
|
763
|
-
}
|
|
764
|
-
core = { appName, bundleId, firebaseProjectId };
|
|
765
|
-
console.log(kleur.gray(` App: ${kleur.white(appName)}`));
|
|
766
|
-
console.log(kleur.gray(` Bundle: ${kleur.white(bundleId)}`));
|
|
767
|
-
if (firebaseProjectId) console.log(kleur.gray(` Project: ${kleur.white(firebaseProjectId)}`));
|
|
768
|
-
} else {
|
|
769
|
-
core = await prompts(coreQuestions, { onCancel: cancel });
|
|
770
|
-
}
|
|
771
|
-
|
|
772
776
|
// ── Firebase: create from scratch (when selected) ─────────────────────────
|
|
773
777
|
let firebaseIncludeWeb = true;
|
|
774
778
|
if (backend === 'firebase' && firebaseSetupMode === 'create') {
|
|
@@ -1251,19 +1255,6 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1251
1255
|
Object.assign(core, api);
|
|
1252
1256
|
}
|
|
1253
1257
|
|
|
1254
|
-
// Resolve targetDir now that we have the app name
|
|
1255
|
-
if (!targetDir) {
|
|
1256
|
-
const folderName = toPackageName(core.appName.trim());
|
|
1257
|
-
targetDir = path.resolve(process.cwd(), folderName);
|
|
1258
|
-
// Guard: derived dir must not already exist
|
|
1259
|
-
if (await fs.pathExists(targetDir)) {
|
|
1260
|
-
const contents = await fs.readdir(targetDir);
|
|
1261
|
-
if (contents.length > 0) {
|
|
1262
|
-
throw new Error(tr('new.firebase.error.dirNotEmpty', { path: targetDir }));
|
|
1263
|
-
}
|
|
1264
|
-
}
|
|
1265
|
-
}
|
|
1266
|
-
|
|
1267
1258
|
// ── Firebase existing project: enable APIs + create Firestore/Storage ───
|
|
1268
1259
|
if (backend === 'firebase' && firebaseSetupMode === 'existing' && core.firebaseProjectId) {
|
|
1269
1260
|
const ps4 = makeProgressSpinner();
|
|
@@ -1274,7 +1265,7 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1274
1265
|
ps4.next(stepProgress('enable-apis', language));
|
|
1275
1266
|
} else if (key === 'enable-apis-warn') {
|
|
1276
1267
|
ps4.warn(`${tr('new.firebase.create.failed')}: APIs`);
|
|
1277
|
-
console.log(kleur.yellow(` ⚠
|
|
1268
|
+
console.log(kleur.yellow(` ⚠ ${tr('new.firebase.existing.apisFailed')} ${(data?.error || '').slice(0, 80)}`));
|
|
1278
1269
|
} else if (key === 'firestore') {
|
|
1279
1270
|
ps4.next(stepProgress('firestore', language));
|
|
1280
1271
|
} else if (key === 'storage') {
|
|
@@ -1285,7 +1276,7 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1285
1276
|
console.log(kleur.cyan(` ${data?.url || ''}`));
|
|
1286
1277
|
} else if (key === 'auth-google-warn') {
|
|
1287
1278
|
ps4.stop();
|
|
1288
|
-
console.log(kleur.yellow(` ⚠
|
|
1279
|
+
console.log(kleur.yellow(` ⚠ ${tr('new.firebase.existing.googleSignInManual')}`));
|
|
1289
1280
|
console.log(kleur.cyan(` ${data?.url || ''}`));
|
|
1290
1281
|
}
|
|
1291
1282
|
},
|
package/lib/commands/run.js
CHANGED
|
@@ -54,7 +54,8 @@ async function runRun(directory, options = {}) {
|
|
|
54
54
|
const args = ['run', ...deviceArgs, ...dartDefines];
|
|
55
55
|
|
|
56
56
|
console.log(kleur.bold(`\n${t('run.launching')}`));
|
|
57
|
-
console.log(kleur.dim(` flutter ${args.join(' ')}
|
|
57
|
+
console.log(kleur.dim(` flutter ${args.join(' ')}`));
|
|
58
|
+
console.log(kleur.dim(` ✦ ${t('run.updateHint.prefix')} `) + kleur.cyan('kasy update') + kleur.dim(` ${t('run.updateHint.suffix')}\n`));
|
|
58
59
|
|
|
59
60
|
return new Promise((resolve, reject) => {
|
|
60
61
|
const proc = spawn('flutter', args, { cwd: projectDir, stdio: 'inherit' });
|
package/lib/utils/checks.js
CHANGED
|
@@ -144,6 +144,17 @@ function getBackendChecks(backend) {
|
|
|
144
144
|
return [...(BACKEND_CHECKS[backend] || [])];
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
function diagnoseFailure(err) {
|
|
148
|
+
const text = `${err?.stderr || ''}\n${err?.stdout || ''}\n${err?.message || ''}`.toLowerCase();
|
|
149
|
+
if (text.includes('xcode') && text.includes('license')) {
|
|
150
|
+
return 'xcodeLicense';
|
|
151
|
+
}
|
|
152
|
+
if (text.includes('xcode-select') || text.includes('command line tools') || text.includes('no developer tools')) {
|
|
153
|
+
return 'xcodeCli';
|
|
154
|
+
}
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
|
|
147
158
|
function extractVersion(stdout, checkName) {
|
|
148
159
|
const raw = (stdout || '').trim();
|
|
149
160
|
if (!raw) return null;
|
|
@@ -200,7 +211,8 @@ async function runSingleCheck(check, options = {}) {
|
|
|
200
211
|
}
|
|
201
212
|
}
|
|
202
213
|
return { ...check, ok: true, version: version || null };
|
|
203
|
-
} catch {
|
|
214
|
+
} catch (err) {
|
|
215
|
+
const diagnosis = diagnoseFailure(err);
|
|
204
216
|
if (check.tryInstall) {
|
|
205
217
|
if (!silent) spinner.text = t(check.tryInstallMessageKey || 'setup.flutterfire.installing');
|
|
206
218
|
try {
|
|
@@ -242,7 +254,10 @@ async function runSingleCheck(check, options = {}) {
|
|
|
242
254
|
if (!silent) {
|
|
243
255
|
if (check.required) {
|
|
244
256
|
const detail = autoInstallFailed ? ` — ${t('checks.install.failed')}` : '';
|
|
245
|
-
|
|
257
|
+
const diagSuffix = diagnosis ? `\n ${kleur.dim(`→ ${t(`checks.diagnostic.${diagnosis}`, { name: check.name })}`)}` : '';
|
|
258
|
+
spinner.fail(t('checks.missing', { name: check.name }) + detail + diagSuffix);
|
|
259
|
+
} else if (diagnosis) {
|
|
260
|
+
spinner.warn(t(`checks.diagnostic.${diagnosis}`, { name: check.name }));
|
|
246
261
|
} else {
|
|
247
262
|
const hint = !check.waitPrompt && check.failHint ? `\n ${kleur.dim(`→ ${check.failHint}`)}` : '';
|
|
248
263
|
if (check.warnMessage) {
|
|
@@ -255,7 +270,7 @@ async function runSingleCheck(check, options = {}) {
|
|
|
255
270
|
}
|
|
256
271
|
}
|
|
257
272
|
|
|
258
|
-
return { ...check, ok: false, autoInstallFailed };
|
|
273
|
+
return { ...check, ok: false, autoInstallFailed, diagnosis };
|
|
259
274
|
}
|
|
260
275
|
}
|
|
261
276
|
|
|
@@ -293,7 +308,10 @@ async function runChecks(checks, title, options = {}) {
|
|
|
293
308
|
const hint = result.failHint ? `\n ${kleur.dim(`→ ${result.failHint}`)}` : '';
|
|
294
309
|
if (result.required) {
|
|
295
310
|
const detail = result.autoInstallFailed ? ` — ${t('checks.install.failed')}` : '';
|
|
296
|
-
|
|
311
|
+
const diagSuffix = result.diagnosis ? `\n ${kleur.dim(`→ ${t(`checks.diagnostic.${result.diagnosis}`, { name: result.name })}`)}` : '';
|
|
312
|
+
console.log(kleur.red(` ✖ ${t('checks.missing', { name: result.name })}${detail}${diagSuffix}${hint}`));
|
|
313
|
+
} else if (result.diagnosis) {
|
|
314
|
+
console.log(kleur.yellow(` ⚠ ${t(`checks.diagnostic.${result.diagnosis}`, { name: result.name })}`));
|
|
297
315
|
} else if (result.warnMessage) {
|
|
298
316
|
console.log(kleur.yellow(` ⚠ ${result.warnMessage}${hint}`));
|
|
299
317
|
} else if (result.warnMessageKey) {
|
package/lib/utils/i18n.js
CHANGED
|
@@ -96,6 +96,8 @@ const MESSAGES = {
|
|
|
96
96
|
'checks.notFound': '{name} not found',
|
|
97
97
|
'checks.flutter.warn': 'Flutter SDK not found. Install Flutter to build and run apps: https://docs.flutter.dev/get-started/install',
|
|
98
98
|
'checks.install.failed': 'auto-install failed — run the command manually',
|
|
99
|
+
'checks.diagnostic.xcodeLicense': '{name} is installed, but Xcode needs the license accepted. Run: sudo xcodebuild -license',
|
|
100
|
+
'checks.diagnostic.xcodeCli': '{name} is installed, but Xcode Command Line Tools are missing. Run: xcode-select --install',
|
|
99
101
|
'banner.title': 'Kasy CLI · Flutter SaaS Generator',
|
|
100
102
|
'welcome.firstRun': 'Welcome to Kasy CLI!',
|
|
101
103
|
'welcome.chooseLanguage': 'First, choose your language:',
|
|
@@ -551,7 +553,9 @@ const MESSAGES = {
|
|
|
551
553
|
'new.api.success.fcm': '• Firebase is required for push notifications (FCM) — configure APNs key in Firebase console',
|
|
552
554
|
'new.api.success.auth': '• Implement the social auth endpoints (Google, Apple) on your backend',
|
|
553
555
|
|
|
554
|
-
'new.outdated.
|
|
556
|
+
'new.outdated.hint': "projects created now won't include the latest improvements.",
|
|
557
|
+
'new.outdated.upgradeNow': 'Upgrade to the latest version before creating? (requires active subscription)',
|
|
558
|
+
'new.outdated.upgraded': 'kasy updated! Run kasy new again.',
|
|
555
559
|
'new.success.title': 'Project created successfully!',
|
|
556
560
|
'new.success.nextSteps': 'Next steps:',
|
|
557
561
|
'new.success.step.cd': 'Go to your project folder:',
|
|
@@ -584,6 +588,8 @@ const MESSAGES = {
|
|
|
584
588
|
// run command
|
|
585
589
|
'cli.command.run.description': '▶ Run the Flutter app using .env (launch.json dart-defines fallback)',
|
|
586
590
|
'run.launching': 'Launching Flutter app...',
|
|
591
|
+
'run.updateHint.prefix': 'Project improvements available —',
|
|
592
|
+
'run.updateHint.suffix': 'to see what\'s new',
|
|
587
593
|
'run.error.notFlutterProject': 'No pubspec.yaml found. Run this command from inside a Flutter project.',
|
|
588
594
|
'run.error.flutterNotFound': 'Flutter not found. Make sure Flutter is installed and on your PATH.',
|
|
589
595
|
|
|
@@ -815,6 +821,8 @@ const MESSAGES = {
|
|
|
815
821
|
'checks.notFound': '{name} nao encontrado',
|
|
816
822
|
'checks.flutter.warn': 'Flutter SDK nao encontrado. Instale o Flutter para compilar e executar apps: https://docs.flutter.dev/get-started/install',
|
|
817
823
|
'checks.install.failed': 'instalacao automatica falhou — execute o comando manualmente',
|
|
824
|
+
'checks.diagnostic.xcodeLicense': '{name} esta instalado, mas o Xcode precisa que a licenca seja aceita. Execute: sudo xcodebuild -license',
|
|
825
|
+
'checks.diagnostic.xcodeCli': '{name} esta instalado, mas faltam as Command Line Tools do Xcode. Execute: xcode-select --install',
|
|
818
826
|
'banner.title': 'Kasy CLI · Gerador Flutter SaaS',
|
|
819
827
|
'welcome.firstRun': 'Bem-vindo ao Kasy CLI!',
|
|
820
828
|
'welcome.chooseLanguage': 'Primeiro, escolha seu idioma:',
|
|
@@ -1270,7 +1278,9 @@ const MESSAGES = {
|
|
|
1270
1278
|
'new.api.success.fcm': '• Firebase e necessario para notificacoes push (FCM) — configure a chave APNs no console do Firebase',
|
|
1271
1279
|
'new.api.success.auth': '• Implemente os endpoints de auth social (Google, Apple) no seu backend',
|
|
1272
1280
|
|
|
1273
|
-
'new.outdated.
|
|
1281
|
+
'new.outdated.hint': 'projetos criados agora não terão as últimas melhorias.',
|
|
1282
|
+
'new.outdated.upgradeNow': 'Atualizar antes de criar? (requer assinatura ativa)',
|
|
1283
|
+
'new.outdated.upgraded': 'kasy atualizado! Rode kasy new novamente.',
|
|
1274
1284
|
'new.success.title': 'Projeto criado com sucesso!',
|
|
1275
1285
|
'new.success.nextSteps': 'Proximos passos:',
|
|
1276
1286
|
'new.success.step.cd': 'Entre na pasta do projeto:',
|
|
@@ -1303,6 +1313,8 @@ const MESSAGES = {
|
|
|
1303
1313
|
// run command
|
|
1304
1314
|
'cli.command.run.description': '▶ Executa o app Flutter usando .env (fallback para dart-defines do launch.json)',
|
|
1305
1315
|
'run.launching': 'Iniciando app Flutter...',
|
|
1316
|
+
'run.updateHint.prefix': 'Melhorias disponíveis para o projeto —',
|
|
1317
|
+
'run.updateHint.suffix': 'para ver o que há de novo',
|
|
1306
1318
|
'run.error.notFlutterProject': 'Nenhum pubspec.yaml encontrado. Execute este comando dentro de um projeto Flutter.',
|
|
1307
1319
|
'run.error.flutterNotFound': 'Flutter nao encontrado. Verifique se o Flutter esta instalado e no PATH.',
|
|
1308
1320
|
|
|
@@ -1534,6 +1546,8 @@ const MESSAGES = {
|
|
|
1534
1546
|
'checks.notFound': '{name} no encontrado',
|
|
1535
1547
|
'checks.flutter.warn': 'Flutter SDK no encontrado. Instala Flutter para compilar y ejecutar apps: https://docs.flutter.dev/get-started/install',
|
|
1536
1548
|
'checks.install.failed': 'instalación automática falló — ejecuta el comando manualmente',
|
|
1549
|
+
'checks.diagnostic.xcodeLicense': '{name} está instalado, pero Xcode necesita que aceptes la licencia. Ejecuta: sudo xcodebuild -license',
|
|
1550
|
+
'checks.diagnostic.xcodeCli': '{name} está instalado, pero faltan las Command Line Tools de Xcode. Ejecuta: xcode-select --install',
|
|
1537
1551
|
'banner.title': 'Kasy CLI · Generador Flutter SaaS',
|
|
1538
1552
|
'welcome.firstRun': '¡Bienvenido a Kasy CLI!',
|
|
1539
1553
|
'welcome.chooseLanguage': 'Primero, elige tu idioma:',
|
|
@@ -1989,7 +2003,9 @@ const MESSAGES = {
|
|
|
1989
2003
|
'new.api.success.fcm': '• Firebase es necesario para notificaciones push (FCM) — configura la clave APNs en la consola de Firebase',
|
|
1990
2004
|
'new.api.success.auth': '• Implementa los endpoints de auth social (Google, Apple) en tu backend',
|
|
1991
2005
|
|
|
1992
|
-
'new.outdated.
|
|
2006
|
+
'new.outdated.hint': 'los proyectos creados ahora no tendrán las últimas mejoras.',
|
|
2007
|
+
'new.outdated.upgradeNow': '¿Actualizar a la última versión antes de crear? (requiere suscripción activa)',
|
|
2008
|
+
'new.outdated.upgraded': '¡kasy actualizado! Ejecuta kasy new nuevamente.',
|
|
1993
2009
|
'new.success.title': '¡Proyecto creado con exito!',
|
|
1994
2010
|
'new.success.nextSteps': 'Proximos pasos:',
|
|
1995
2011
|
'new.success.step.cd': 'Ve a la carpeta del proyecto:',
|
|
@@ -2022,6 +2038,8 @@ const MESSAGES = {
|
|
|
2022
2038
|
// run command
|
|
2023
2039
|
'cli.command.run.description': '▶ Ejecuta el app Flutter usando .env (fallback a dart-defines del launch.json)',
|
|
2024
2040
|
'run.launching': 'Iniciando app Flutter...',
|
|
2041
|
+
'run.updateHint.prefix': 'Mejoras disponibles para el proyecto —',
|
|
2042
|
+
'run.updateHint.suffix': 'para ver las novedades',
|
|
2025
2043
|
'run.error.notFlutterProject': 'No se encontro pubspec.yaml. Ejecuta este comando dentro de un proyecto Flutter.',
|
|
2026
2044
|
'run.error.flutterNotFound': 'Flutter no encontrado. Verifica que Flutter este instalado y en el PATH.',
|
|
2027
2045
|
|
package/lib/utils/ui.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UI wrapper around @clack/prompts.
|
|
3
|
+
*
|
|
4
|
+
* Goal: centralize prompt/styling primitives so commands don't import
|
|
5
|
+
* `@clack/prompts` directly. This keeps the migration from the legacy
|
|
6
|
+
* `prompts` package incremental — commands can adopt this module one
|
|
7
|
+
* by one without touching the rest.
|
|
8
|
+
*
|
|
9
|
+
* Cancel handling: every prompt auto-detects Ctrl+C via `isCancel`
|
|
10
|
+
* and calls the optional `onCancel` callback (or exits with code 0
|
|
11
|
+
* and a friendly message). Callers don't need to wire `onCancel`
|
|
12
|
+
* in each call like they did with the old `prompts` library.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const clack = require('@clack/prompts');
|
|
16
|
+
|
|
17
|
+
function handleCancel(result, onCancel) {
|
|
18
|
+
if (clack.isCancel(result)) {
|
|
19
|
+
if (typeof onCancel === 'function') {
|
|
20
|
+
onCancel();
|
|
21
|
+
} else {
|
|
22
|
+
clack.cancel('Operação cancelada.');
|
|
23
|
+
}
|
|
24
|
+
process.exit(0);
|
|
25
|
+
}
|
|
26
|
+
return result;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function select({ message, options, initialValue, onCancel }) {
|
|
30
|
+
const result = await clack.select({ message, options, initialValue });
|
|
31
|
+
return handleCancel(result, onCancel);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function text({ message, placeholder, initialValue, defaultValue, validate, onCancel }) {
|
|
35
|
+
const result = await clack.text({
|
|
36
|
+
message,
|
|
37
|
+
placeholder,
|
|
38
|
+
initialValue,
|
|
39
|
+
defaultValue,
|
|
40
|
+
validate: validate
|
|
41
|
+
? (value) => {
|
|
42
|
+
const out = validate(value);
|
|
43
|
+
if (out === true || out == null) return undefined;
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
: undefined,
|
|
47
|
+
});
|
|
48
|
+
return handleCancel(result, onCancel);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function confirm({ message, initialValue = true, onCancel }) {
|
|
52
|
+
const result = await clack.confirm({ message, initialValue });
|
|
53
|
+
return handleCancel(result, onCancel);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function multiselect({ message, options, initialValues, required = false, onCancel }) {
|
|
57
|
+
const result = await clack.multiselect({ message, options, initialValues, required });
|
|
58
|
+
return handleCancel(result, onCancel);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function intro(title) { clack.intro(title); }
|
|
62
|
+
function outro(message) { clack.outro(message); }
|
|
63
|
+
function note(message, title) { clack.note(message, title); }
|
|
64
|
+
function cancel(message) { clack.cancel(message); }
|
|
65
|
+
|
|
66
|
+
function spinner() { return clack.spinner(); }
|
|
67
|
+
|
|
68
|
+
const log = clack.log;
|
|
69
|
+
|
|
70
|
+
module.exports = {
|
|
71
|
+
select,
|
|
72
|
+
text,
|
|
73
|
+
confirm,
|
|
74
|
+
multiselect,
|
|
75
|
+
intro,
|
|
76
|
+
outro,
|
|
77
|
+
note,
|
|
78
|
+
cancel,
|
|
79
|
+
spinner,
|
|
80
|
+
log,
|
|
81
|
+
isCancel: clack.isCancel,
|
|
82
|
+
};
|
package/lib/utils/updates.js
CHANGED
|
@@ -105,28 +105,26 @@ async function warnIfOutdatedBeforeNew(t) {
|
|
|
105
105
|
const cache = (readConfig().updateCheck) || {};
|
|
106
106
|
if (!cache.latestVersion || !isNewer(cache.latestVersion, pkg.version)) return;
|
|
107
107
|
|
|
108
|
+
const hint = t ? t('new.outdated.hint') : 'projetos criados agora não terão as últimas melhorias.';
|
|
108
109
|
console.log('');
|
|
109
110
|
console.log(
|
|
110
|
-
kleur.bgYellow().black('
|
|
111
|
+
kleur.bgYellow().black(' UPDATE ') + ' ' +
|
|
111
112
|
kleur.bold(`v${cache.latestVersion} disponível`) +
|
|
112
|
-
kleur.dim(`
|
|
113
|
+
kleur.dim(` — ${hint}`)
|
|
113
114
|
);
|
|
114
|
-
console.log(kleur.dim(' Projetos novos são criados com a versão que você tem instalada.'));
|
|
115
|
-
console.log(kleur.dim(' Para criar com a versão mais recente: ') + kleur.cyan('kasy upgrade') + kleur.dim(' (requer assinatura ativa)'));
|
|
116
115
|
console.log('');
|
|
117
116
|
|
|
118
|
-
const {
|
|
117
|
+
const { upgrade } = await prompts({
|
|
119
118
|
type: 'confirm',
|
|
120
|
-
name: '
|
|
121
|
-
message: t ? t('new.outdated.
|
|
122
|
-
initial:
|
|
119
|
+
name: 'upgrade',
|
|
120
|
+
message: t ? t('new.outdated.upgradeNow') : 'Atualizar antes de criar? (requer assinatura ativa)',
|
|
121
|
+
initial: false,
|
|
123
122
|
});
|
|
124
123
|
|
|
125
|
-
if (
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
process.exit(0);
|
|
124
|
+
if (upgrade) {
|
|
125
|
+
const { spawnSync } = require('node:child_process');
|
|
126
|
+
const result = spawnSync('kasy', ['upgrade'], { stdio: 'inherit', shell: true });
|
|
127
|
+
process.exit(result.status ?? 0);
|
|
130
128
|
}
|
|
131
129
|
} catch {
|
|
132
130
|
// Nunca travar o kasy new por causa do aviso de versão
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kasy-cli",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.3",
|
|
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"
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"test:localize-docs": "node ./test/localize-release-docs.test.js"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
+
"@clack/prompts": "^1.4.0",
|
|
47
48
|
"commander": "^12.0.0",
|
|
48
49
|
"fs-extra": "^11.2.0",
|
|
49
50
|
"gradient-string": "^1.2.0",
|