kasy-cli 1.5.1 → 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,10 +69,12 @@ 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');
75
76
  const { promptLanguage } = require('../utils/prompts');
77
+ const { warnIfOutdatedBeforeNew } = require('../utils/updates');
76
78
  const {
77
79
  getBaseChecks,
78
80
  getPlatformChecks,
@@ -517,6 +519,11 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
517
519
  const tr = createTranslator(language);
518
520
  const cancel = () => onCancel(tr);
519
521
 
522
+ // ── 1b. Version check — warn if outdated, let user decide ───────────────
523
+ if (!yes) {
524
+ await warnIfOutdatedBeforeNew(tr);
525
+ }
526
+
520
527
  // ── 2. Environment checks (non-blocking — only warnings) ────────────────
521
528
  const envChecks = [...getBaseChecks(), ...getPlatformChecks()];
522
529
  await runChecks(envChecks, tr('new.checks.environment'), {
@@ -528,6 +535,8 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
528
535
 
529
536
  printBanner(tr);
530
537
 
538
+ ui.intro(kleur.bold(tr('new.subtitle2')));
539
+
531
540
  // Whether an explicit target directory was provided by the user
532
541
  const hasExplicitDir = directory && directory !== '.';
533
542
 
@@ -547,21 +556,16 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
547
556
  // ── 3. Backend selection (support pre-selection via --backend flag) ─────
548
557
  let backend = normalizeBackend(backendHint);
549
558
  if (!backend) {
550
- const { backend: selectedBackend } = await prompts(
551
- {
552
- type: 'select',
553
- name: 'backend',
554
- message: tr('new.q.backend'),
555
- choices: [
556
- { title: '🔥 Firebase', description: tr('new.q.backend.firebase.desc'), value: 'firebase' },
557
- { title: '🟢 Supabase', description: tr('new.q.backend.supabase.desc'), value: 'supabase' },
558
- { title: '🔗 API REST', description: tr('new.q.backend.api.desc'), value: 'api' },
559
- ],
560
- initial: 0,
561
- },
562
- { onCancel: cancel }
563
- );
564
- 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
+ });
565
569
  } else {
566
570
  const backendLabel = { firebase: '🔥 Firebase', supabase: '🟢 Supabase', api: '🔗 API REST' }[backend] || backend;
567
571
  console.log(kleur.gray(` Backend: ${kleur.white(backendLabel)}`));
@@ -595,65 +599,160 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
595
599
  }
596
600
  }
597
601
 
598
- // ── 3c. Wizard mode Quick (few questions) or Full (all options) ──────────
599
- let isQuick = yes; // --yes implies Quick mode
600
- if (!yes) {
601
- const { wizardMode } = await prompts(
602
- {
603
- type: 'select',
604
- name: 'wizardMode',
605
- message: tr('new.q.mode'),
606
- choices: [
607
- { title: tr('new.q.mode.quick'), value: 'quick' },
608
- { title: tr('new.q.mode.advanced'), value: 'advanced' },
609
- ],
610
- initial: 0,
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');
611
649
  },
612
- { onCancel: cancel }
613
- );
614
- isQuick = wizardMode === 'quick';
650
+ onCancel: cancel,
651
+ });
652
+
653
+ core = { appName, bundleId };
615
654
  }
616
655
 
617
- // ── 4b. Firebase setup mode (create vs existing) — ask first so prereqs are correct ─
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
+
668
+ // ── Firebase setup mode (create vs existing) ────────────────────────────────
618
669
  // Firebase backend: full setup. Supabase/API: Firebase only for push notifications (FCM).
619
670
  let firebaseSetupMode = 'existing';
620
671
  if (!yes) {
621
672
  if (backend === 'firebase') {
622
- const { setupMode } = await prompts(
623
- {
624
- type: 'select',
625
- name: 'setupMode',
626
- message: tr('new.firebase.q.setupMode'),
627
- choices: [
628
- { title: tr('new.firebase.q.setupMode.create'), value: 'create' },
629
- { title: tr('new.firebase.q.setupMode.existing'), value: 'existing' },
630
- ],
631
- initial: 0,
632
- },
633
- { onCancel: cancel }
634
- );
635
- 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
+ });
636
682
  } else if (backend === 'supabase' || backend === 'api') {
637
- const { setupMode } = await prompts(
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
+ });
693
+ }
694
+ }
695
+
696
+ // ── Wizard mode — Quick (few questions) or Full (all options) ───────────────
697
+ // Asked after app name + setup context are clear, so the user understands what it controls.
698
+ let isQuick = yes; // --yes implies Quick mode
699
+ if (!yes) {
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
+ });
709
+ isQuick = wizardMode === 'quick';
710
+ }
711
+
712
+ // ── Backend-specific prerequisites (dynamic: only show what's not yet verified) ─
713
+ printPrerequisites(tr, backend, firebaseSetupMode, backendCheckResults);
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(
638
739
  {
639
- type: 'select',
640
- name: 'setupMode',
641
- message: tr('new.firebase.q.setupMode.push'),
642
- choices: [
643
- { title: tr('new.firebase.q.setupMode.create'), value: 'create' },
644
- { title: tr('new.firebase.q.setupMode.existing'), value: 'existing' },
645
- ],
646
- initial: 0,
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')),
647
745
  },
648
746
  { onCancel: cancel }
649
747
  );
650
- firebaseSetupMode = setupMode;
748
+ firebaseProjectId = pid?.trim() || '';
749
+ }
750
+ if (firebaseProjectId) {
751
+ core.firebaseProjectId = firebaseProjectId;
752
+ console.log(kleur.gray(` Project: ${kleur.white(firebaseProjectId)}`));
651
753
  }
652
754
  }
653
755
 
654
- // ── 4c. Backend-specific prerequisites (dynamic: only show what's not yet verified) ─
655
- printPrerequisites(tr, backend, firebaseSetupMode, backendCheckResults);
656
-
657
756
  // ── Firebase region — Quick mode uses default (us-central1) ──────────
658
757
  let firebaseRegion = 'us-central1';
659
758
  if (backend === 'firebase' && !isQuick) {
@@ -674,93 +773,6 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
674
773
  firebaseRegion = region || 'us-central1';
675
774
  }
676
775
 
677
- // ── Core questions (appName, bundleId) ────────────────────────────────────
678
- const coreQuestions = [
679
- {
680
- type: 'text',
681
- name: 'appName',
682
- message: tr('new.firebase.q.appName'),
683
- hint: tr('new.firebase.q.appName.hint'),
684
- initial: hasExplicitDir ? path.basename(targetDir) : '',
685
- validate: (v) => (v && v.trim() ? true : tr('new.firebase.q.appName.required')),
686
- },
687
- {
688
- type: 'text',
689
- name: 'bundleId',
690
- message: tr('new.firebase.q.bundleId'),
691
- hint: tr('new.firebase.q.bundleId.hint'),
692
- initial: (prev, values) => {
693
- const name = (values?.appName || prev || '').trim();
694
- if (!name) return 'com.example.app';
695
- const slug = name
696
- .normalize('NFD')
697
- .replace(/[\u0300-\u036f]/g, '')
698
- .toLowerCase()
699
- .replace(/[^a-z0-9]/g, '');
700
- if (!slug || /^\d/.test(slug)) return 'com.example.app';
701
- return `com.${slug}.app`;
702
- },
703
- validate: (v) => {
704
- if (!v || !v.trim()) return tr('new.firebase.q.bundleId.required');
705
- return /^[a-zA-Z][\w]*(\.[a-zA-Z][\w]*)+$/.test(v.trim())
706
- ? true
707
- : tr('new.firebase.q.bundleId.invalid');
708
- },
709
- },
710
- ];
711
- const needFirebaseProjectIdNow =
712
- (backend === 'firebase' && firebaseSetupMode === 'existing') ||
713
- ((backend === 'supabase' || backend === 'api') && firebaseSetupMode === 'existing');
714
- if (needFirebaseProjectIdNow) {
715
- coreQuestions.push({
716
- type: 'text',
717
- name: 'firebaseProjectId',
718
- message: tr('new.firebase.q.projectId'),
719
- hint: tr('new.firebase.q.projectId.hint') + (backend !== 'firebase' ? ' (FCM + Remote Config)' : ''),
720
- validate: (v) => {
721
- if (!v || !v.trim()) return tr('new.firebase.q.projectId.required');
722
- const id = v.trim();
723
- if (id.length < 6 || id.length > 30) return 'ID deve ter entre 6 e 30 caracteres (ex: meu-app-123)';
724
- 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)';
725
- return true;
726
- },
727
- });
728
- }
729
- let core;
730
- if (yes) {
731
- if (!hasExplicitDir) {
732
- console.error(kleur.red(`\n ✗ --yes requires an app name: kasy new MyApp --yes\n`));
733
- process.exit(1);
734
- }
735
- const appName = path.basename(targetDir);
736
- const slug = appName
737
- .normalize('NFD')
738
- .replace(/[\u0300-\u036f]/g, '')
739
- .toLowerCase()
740
- .replace(/[^a-z0-9]/g, '');
741
- const bundleId = (slug && !/^\d/.test(slug)) ? `com.${slug}.app` : 'com.example.app';
742
- let firebaseProjectId = projectHint?.trim() || '';
743
- if (!firebaseProjectId && backend === 'firebase') {
744
- const { pid } = await prompts(
745
- {
746
- type: 'text',
747
- name: 'pid',
748
- message: tr('new.firebase.q.projectId'),
749
- hint: tr('new.firebase.q.projectId.hint'),
750
- validate: (v) => (v && v.trim() ? true : tr('new.firebase.q.projectId.required')),
751
- },
752
- { onCancel: cancel }
753
- );
754
- firebaseProjectId = pid?.trim() || '';
755
- }
756
- core = { appName, bundleId, firebaseProjectId };
757
- console.log(kleur.gray(` App: ${kleur.white(appName)}`));
758
- console.log(kleur.gray(` Bundle: ${kleur.white(bundleId)}`));
759
- if (firebaseProjectId) console.log(kleur.gray(` Project: ${kleur.white(firebaseProjectId)}`));
760
- } else {
761
- core = await prompts(coreQuestions, { onCancel: cancel });
762
- }
763
-
764
776
  // ── Firebase: create from scratch (when selected) ─────────────────────────
765
777
  let firebaseIncludeWeb = true;
766
778
  if (backend === 'firebase' && firebaseSetupMode === 'create') {
@@ -1243,19 +1255,6 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
1243
1255
  Object.assign(core, api);
1244
1256
  }
1245
1257
 
1246
- // Resolve targetDir now that we have the app name
1247
- if (!targetDir) {
1248
- const folderName = toPackageName(core.appName.trim());
1249
- targetDir = path.resolve(process.cwd(), folderName);
1250
- // Guard: derived dir must not already exist
1251
- if (await fs.pathExists(targetDir)) {
1252
- const contents = await fs.readdir(targetDir);
1253
- if (contents.length > 0) {
1254
- throw new Error(tr('new.firebase.error.dirNotEmpty', { path: targetDir }));
1255
- }
1256
- }
1257
- }
1258
-
1259
1258
  // ── Firebase existing project: enable APIs + create Firestore/Storage ───
1260
1259
  if (backend === 'firebase' && firebaseSetupMode === 'existing' && core.firebaseProjectId) {
1261
1260
  const ps4 = makeProgressSpinner();
@@ -1266,7 +1265,7 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
1266
1265
  ps4.next(stepProgress('enable-apis', language));
1267
1266
  } else if (key === 'enable-apis-warn') {
1268
1267
  ps4.warn(`${tr('new.firebase.create.failed')}: APIs`);
1269
- 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)}`));
1270
1269
  } else if (key === 'firestore') {
1271
1270
  ps4.next(stepProgress('firestore', language));
1272
1271
  } else if (key === 'storage') {
@@ -1277,7 +1276,7 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
1277
1276
  console.log(kleur.cyan(` ${data?.url || ''}`));
1278
1277
  } else if (key === 'auth-google-warn') {
1279
1278
  ps4.stop();
1280
- 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')}`));
1281
1280
  console.log(kleur.cyan(` ${data?.url || ''}`));
1282
1281
  }
1283
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:',
@@ -132,6 +134,7 @@ const MESSAGES = {
132
134
  'new.supabase.q.create.existing': '📂 Use existing project',
133
135
  'new.firebase.q.setupMode': 'How do you want to set up Firebase?',
134
136
  'new.firebase.q.setupMode.push': 'Firebase project for push notifications (FCM):',
137
+ 'new.firebase.q.setupMode.push.explain': 'Push notifications (FCM) require a Firebase project, even when your main backend is Supabase or API REST.',
135
138
  'new.firebase.q.setupMode.create': '✨ Create from scratch (recommended for beginners)',
136
139
  'new.firebase.q.setupMode.existing': '📂 Use existing project',
137
140
  'new.firebase.q.region': 'Where are most of your app users located?',
@@ -510,6 +513,8 @@ const MESSAGES = {
510
513
  'new.firebase.interactive.billingWaiting': 'Checking Blaze status...',
511
514
  'new.firebase.interactive.billingTimeout': 'Blaze plan not confirmed after timeout. Deploy skipped — run manually when ready.',
512
515
  'new.firebase.interactive.authWarn': 'Could not enable Email/Password and Anonymous auth automatically. Enable manually:',
516
+ 'new.firebase.existing.apisFailed': 'Could not activate APIs:',
517
+ 'new.firebase.existing.googleSignInManual': 'Google Sign-In: enable manually in Authentication → Sign-in method → Google',
513
518
 
514
519
  'new.firebase.interactive.ready': 'Ready to deploy Push Notifications + Security Rules now?',
515
520
  'new.firebase.interactive.deploying': 'Deploying Cloud Functions + Firestore/Storage rules...',
@@ -548,6 +553,9 @@ const MESSAGES = {
548
553
  'new.api.success.fcm': '• Firebase is required for push notifications (FCM) — configure APNs key in Firebase console',
549
554
  'new.api.success.auth': '• Implement the social auth endpoints (Google, Apple) on your backend',
550
555
 
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.',
551
559
  'new.success.title': 'Project created successfully!',
552
560
  'new.success.nextSteps': 'Next steps:',
553
561
  'new.success.step.cd': 'Go to your project folder:',
@@ -580,6 +588,8 @@ const MESSAGES = {
580
588
  // run command
581
589
  'cli.command.run.description': '▶ Run the Flutter app using .env (launch.json dart-defines fallback)',
582
590
  'run.launching': 'Launching Flutter app...',
591
+ 'run.updateHint.prefix': 'Project improvements available —',
592
+ 'run.updateHint.suffix': 'to see what\'s new',
583
593
  'run.error.notFlutterProject': 'No pubspec.yaml found. Run this command from inside a Flutter project.',
584
594
  'run.error.flutterNotFound': 'Flutter not found. Make sure Flutter is installed and on your PATH.',
585
595
 
@@ -811,6 +821,8 @@ const MESSAGES = {
811
821
  'checks.notFound': '{name} nao encontrado',
812
822
  'checks.flutter.warn': 'Flutter SDK nao encontrado. Instale o Flutter para compilar e executar apps: https://docs.flutter.dev/get-started/install',
813
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',
814
826
  'banner.title': 'Kasy CLI · Gerador Flutter SaaS',
815
827
  'welcome.firstRun': 'Bem-vindo ao Kasy CLI!',
816
828
  'welcome.chooseLanguage': 'Primeiro, escolha seu idioma:',
@@ -847,6 +859,7 @@ const MESSAGES = {
847
859
  'new.supabase.q.create.existing': '📂 Usar projeto existente',
848
860
  'new.firebase.q.setupMode': 'Como deseja configurar o Firebase?',
849
861
  'new.firebase.q.setupMode.push': 'Projeto Firebase para notificações push (FCM):',
862
+ 'new.firebase.q.setupMode.push.explain': 'Notificações push (FCM) precisam de um projeto Firebase, mesmo quando seu backend principal é Supabase ou API REST.',
850
863
  'new.firebase.q.setupMode.create': '✨ Criar do zero (recomendado para iniciantes)',
851
864
  'new.firebase.q.setupMode.existing': '📂 Usar projeto existente',
852
865
  'new.firebase.q.region': 'Onde está a maioria dos usuários do seu app?',
@@ -1225,6 +1238,8 @@ const MESSAGES = {
1225
1238
  'new.firebase.interactive.billingWaiting': 'Verificando status do Blaze...',
1226
1239
  'new.firebase.interactive.billingTimeout': 'Plano Blaze nao confirmado apos o tempo limite. Deploy ignorado — rode manualmente quando estiver pronto.',
1227
1240
  'new.firebase.interactive.authWarn': 'Nao foi possivel ativar Email/Senha e Anonimo automaticamente. Ative manualmente:',
1241
+ 'new.firebase.existing.apisFailed': 'Nao foi possivel ativar APIs:',
1242
+ 'new.firebase.existing.googleSignInManual': 'Google Sign-In: ative manualmente em Authentication → Sign-in method → Google',
1228
1243
 
1229
1244
  'new.firebase.interactive.ready': 'Pronto para publicar Push Notifications + Regras agora?',
1230
1245
  'new.firebase.interactive.deploying': 'Fazendo deploy das Cloud Functions + Regras (Storage e Firestore)...',
@@ -1263,6 +1278,9 @@ const MESSAGES = {
1263
1278
  'new.api.success.fcm': '• Firebase e necessario para notificacoes push (FCM) — configure a chave APNs no console do Firebase',
1264
1279
  'new.api.success.auth': '• Implemente os endpoints de auth social (Google, Apple) no seu backend',
1265
1280
 
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.',
1266
1284
  'new.success.title': 'Projeto criado com sucesso!',
1267
1285
  'new.success.nextSteps': 'Proximos passos:',
1268
1286
  'new.success.step.cd': 'Entre na pasta do projeto:',
@@ -1295,6 +1313,8 @@ const MESSAGES = {
1295
1313
  // run command
1296
1314
  'cli.command.run.description': '▶ Executa o app Flutter usando .env (fallback para dart-defines do launch.json)',
1297
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',
1298
1318
  'run.error.notFlutterProject': 'Nenhum pubspec.yaml encontrado. Execute este comando dentro de um projeto Flutter.',
1299
1319
  'run.error.flutterNotFound': 'Flutter nao encontrado. Verifique se o Flutter esta instalado e no PATH.',
1300
1320
 
@@ -1526,6 +1546,8 @@ const MESSAGES = {
1526
1546
  'checks.notFound': '{name} no encontrado',
1527
1547
  'checks.flutter.warn': 'Flutter SDK no encontrado. Instala Flutter para compilar y ejecutar apps: https://docs.flutter.dev/get-started/install',
1528
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',
1529
1551
  'banner.title': 'Kasy CLI · Generador Flutter SaaS',
1530
1552
  'welcome.firstRun': '¡Bienvenido a Kasy CLI!',
1531
1553
  'welcome.chooseLanguage': 'Primero, elige tu idioma:',
@@ -1562,6 +1584,7 @@ const MESSAGES = {
1562
1584
  'new.supabase.q.create.existing': '📂 Usar proyecto existente',
1563
1585
  'new.firebase.q.setupMode': '¿Cómo quieres configurar Firebase?',
1564
1586
  'new.firebase.q.setupMode.push': 'Proyecto Firebase para notificaciones push (FCM):',
1587
+ 'new.firebase.q.setupMode.push.explain': 'Las notificaciones push (FCM) requieren un proyecto Firebase, incluso cuando tu backend principal es Supabase o API REST.',
1565
1588
  'new.firebase.q.setupMode.create': '✨ Crear desde cero (recomendado para principiantes)',
1566
1589
  'new.firebase.q.setupMode.existing': '📂 Usar proyecto existente',
1567
1590
  'new.firebase.q.region': '¿Dónde están la mayoría de los usuarios de tu app?',
@@ -1940,6 +1963,8 @@ const MESSAGES = {
1940
1963
  'new.firebase.interactive.billingWaiting': 'Verificando estado del Blaze...',
1941
1964
  'new.firebase.interactive.billingTimeout': 'Plan Blaze no confirmado tras el tiempo límite. Despliegue omitido — ejecuta manualmente cuando estés listo.',
1942
1965
  'new.firebase.interactive.authWarn': 'No se pudo activar Email/Contraseña y Anónimo automáticamente. Actívalos manualmente:',
1966
+ 'new.firebase.existing.apisFailed': 'No se pudieron activar las APIs:',
1967
+ 'new.firebase.existing.googleSignInManual': 'Google Sign-In: activa manualmente en Authentication → Sign-in method → Google',
1943
1968
 
1944
1969
  'new.firebase.interactive.ready': '¿Listo para publicar Push Notifications + Reglas ahora?',
1945
1970
  'new.firebase.interactive.deploying': 'Desplegando Cloud Functions + Reglas (Storage y Firestore)...',
@@ -1978,6 +2003,9 @@ const MESSAGES = {
1978
2003
  'new.api.success.fcm': '• Firebase es necesario para notificaciones push (FCM) — configura la clave APNs en la consola de Firebase',
1979
2004
  'new.api.success.auth': '• Implementa los endpoints de auth social (Google, Apple) en tu backend',
1980
2005
 
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.',
1981
2009
  'new.success.title': '¡Proyecto creado con exito!',
1982
2010
  'new.success.nextSteps': 'Proximos pasos:',
1983
2011
  'new.success.step.cd': 'Ve a la carpeta del proyecto:',
@@ -2010,6 +2038,8 @@ const MESSAGES = {
2010
2038
  // run command
2011
2039
  'cli.command.run.description': '▶ Ejecuta el app Flutter usando .env (fallback a dart-defines del launch.json)',
2012
2040
  'run.launching': 'Iniciando app Flutter...',
2041
+ 'run.updateHint.prefix': 'Mejoras disponibles para el proyecto —',
2042
+ 'run.updateHint.suffix': 'para ver las novedades',
2013
2043
  'run.error.notFlutterProject': 'No se encontro pubspec.yaml. Ejecuta este comando dentro de un proyecto Flutter.',
2014
2044
  'run.error.flutterNotFound': 'Flutter no encontrado. Verifica que Flutter este instalado y en el PATH.',
2015
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
+ };
@@ -4,6 +4,7 @@ const https = require('node:https');
4
4
  const path = require('node:path');
5
5
  const { existsSync, readJsonSync, writeJsonSync, ensureDirSync } = require('fs-extra');
6
6
  const kleur = require('kleur');
7
+ const prompts = require('prompts');
7
8
  const pkg = require('../../package.json');
8
9
  const { CONFIG_PATH } = require('./license');
9
10
 
@@ -94,4 +95,40 @@ async function checkForUpdates() {
94
95
  }
95
96
  }
96
97
 
97
- module.exports = { checkForUpdates };
98
+ /**
99
+ * Chamado antes de kasy new. Se houver versão mais nova no cache,
100
+ * mostra aviso e pergunta se quer continuar ou sair para atualizar.
101
+ * Nunca bloqueia — se o usuário ignorar, o fluxo segue normalmente.
102
+ */
103
+ async function warnIfOutdatedBeforeNew(t) {
104
+ try {
105
+ const cache = (readConfig().updateCheck) || {};
106
+ if (!cache.latestVersion || !isNewer(cache.latestVersion, pkg.version)) return;
107
+
108
+ const hint = t ? t('new.outdated.hint') : 'projetos criados agora não terão as últimas melhorias.';
109
+ console.log('');
110
+ console.log(
111
+ kleur.bgYellow().black(' UPDATE ') + ' ' +
112
+ kleur.bold(`v${cache.latestVersion} disponível`) +
113
+ kleur.dim(` — ${hint}`)
114
+ );
115
+ console.log('');
116
+
117
+ const { upgrade } = await prompts({
118
+ type: 'confirm',
119
+ name: 'upgrade',
120
+ message: t ? t('new.outdated.upgradeNow') : 'Atualizar antes de criar? (requer assinatura ativa)',
121
+ initial: false,
122
+ });
123
+
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);
128
+ }
129
+ } catch {
130
+ // Nunca travar o kasy new por causa do aviso de versão
131
+ }
132
+ }
133
+
134
+ module.exports = { checkForUpdates, warnIfOutdatedBeforeNew };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kasy-cli",
3
- "version": "1.5.1",
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",