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.
@@ -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
- const { backend: selectedBackend } = await prompts(
557
- {
558
- type: 'select',
559
- name: 'backend',
560
- message: tr('new.q.backend'),
561
- choices: [
562
- { title: '🔥 Firebase', description: tr('new.q.backend.firebase.desc'), value: 'firebase' },
563
- { title: '🟢 Supabase', description: tr('new.q.backend.supabase.desc'), value: 'supabase' },
564
- { title: '🔗 API REST', description: tr('new.q.backend.api.desc'), value: 'api' },
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
- const { setupMode } = await prompts(
610
- {
611
- type: 'select',
612
- name: 'setupMode',
613
- message: tr('new.firebase.q.setupMode'),
614
- choices: [
615
- { title: tr('new.firebase.q.setupMode.create'), value: 'create' },
616
- { title: tr('new.firebase.q.setupMode.existing'), value: 'existing' },
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
- console.log(kleur.dim(`\n ℹ ${tr('new.firebase.q.setupMode.push.explain')}\n`));
625
- const { setupMode } = await prompts(
626
- {
627
- type: 'select',
628
- name: 'setupMode',
629
- message: tr('new.firebase.q.setupMode.push'),
630
- choices: [
631
- { title: tr('new.firebase.q.setupMode.create'), value: 'create' },
632
- { title: tr('new.firebase.q.setupMode.existing'), value: 'existing' },
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 { wizardMode } = await prompts(
647
- {
648
- type: 'select',
649
- name: 'wizardMode',
650
- message: tr('new.q.mode'),
651
- choices: [
652
- { title: tr('new.q.mode.quick'), value: 'quick' },
653
- { title: tr('new.q.mode.advanced'), value: 'advanced' },
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(` ⚠ Não foi possível ativar APIs: ${(data?.error || '').slice(0, 80)}`));
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(` ⚠ Google Sign-In: ative manualmente em Authentication → Sign-in method → Google`));
1279
+ console.log(kleur.yellow(` ⚠ ${tr('new.firebase.existing.googleSignInManual')}`));
1289
1280
  console.log(kleur.cyan(` ${data?.url || ''}`));
1290
1281
  }
1291
1282
  },
@@ -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(' ')}\n`));
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' });
@@ -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
- spinner.fail(t('checks.missing', { name: check.name }) + detail);
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
- console.log(kleur.red(` ✖ ${t('checks.missing', { name: result.name })}${detail}${hint}`));
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.continueAnyway': 'Continue creating the project with the current version?',
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.continueAnyway': 'Continuar criando o projeto com a versao atual?',
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.continueAnyway': '¿Continuar creando el proyecto con la version actual?',
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
 
@@ -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
+ };
@@ -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(' VERSÃO DESATUALIZADA ') + ' ' +
111
+ kleur.bgYellow().black(' UPDATE ') + ' ' +
111
112
  kleur.bold(`v${cache.latestVersion} disponível`) +
112
- kleur.dim(` (você tem v${pkg.version})`)
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 { proceed } = await prompts({
117
+ const { upgrade } = await prompts({
119
118
  type: 'confirm',
120
- name: 'proceed',
121
- message: t ? t('new.outdated.continueAnyway') : 'Continuar com a versão atual?',
122
- initial: true,
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 (proceed === false) {
126
- console.log('');
127
- console.log(kleur.cyan(' kasy upgrade'));
128
- console.log('');
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.2",
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",