kasy-cli 1.21.3 → 1.21.4

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/bin/kasy.js CHANGED
@@ -437,7 +437,17 @@ function buildProgram(language) {
437
437
  .action(() => {
438
438
  const { spawnSync } = require('node:child_process');
439
439
  const path = require('node:path');
440
+ const fs = require('node:fs');
440
441
  printCompactHeader(t);
442
+
443
+ // Read the version straight off disk (not the cached require) so we can
444
+ // report the real before/after — npm overwrites this package.json in place.
445
+ const pkgPath = path.join(__dirname, '..', 'package.json');
446
+ const readVersion = () => {
447
+ try { return JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version; } catch { return null; }
448
+ };
449
+ const before = readVersion();
450
+ if (before) console.log(kleur.dim(t('cli.command.upgrade.current', { version: before })));
441
451
  console.log(kleur.cyan(t('cli.command.upgrade.running')) + '\n');
442
452
  // Update INTO the prefix the CLI actually lives in. The installer uses
443
453
  // `npm install -g --prefix ~/.kasy`, so a bare `npm install -g` would
@@ -457,7 +467,12 @@ function buildProgram(language) {
457
467
  if (prefix) args.push('--prefix', prefix);
458
468
  const result = spawnSync('npm', args, { stdio: 'inherit', shell: true });
459
469
  if (result.status === 0) {
460
- console.log('\n' + kleur.green('✓ ' + t('cli.command.upgrade.done')) + '\n');
470
+ const after = readVersion();
471
+ let msg;
472
+ if (after && before && after !== before) msg = t('cli.command.upgrade.now', { version: after });
473
+ else if (after) msg = t('cli.command.upgrade.already', { version: after });
474
+ else msg = t('cli.command.upgrade.done');
475
+ console.log('\n' + kleur.green('✓ ' + msg) + '\n');
461
476
  } else {
462
477
  process.exitCode = result.status ?? 1;
463
478
  }
@@ -281,22 +281,28 @@ function printCreateFromScratchStatus(result, tr) {
281
281
  ui.log.success(tr('new.sha1.added'));
282
282
  }
283
283
 
284
- if (result.firestoreCreated) {
285
- ui.log.success(tr('new.firestore.created'));
286
- } else {
287
- const msg = result.firestoreError
288
- ? tr('new.firestore.notCreated.error', { error: (result.firestoreError || '').slice(0, 100) })
289
- : tr('new.firestore.notCreated');
290
- ui.log.warn(`${msg}\n${tr('new.activateManually')}\n${kleur.cyan(result.firestoreUrl)}`);
284
+ // Firestore/Storage only apply to the Firebase backend. null = not applicable
285
+ // (fcmOnly: Supabase/API use their own DB/storage), so skip the lines entirely.
286
+ if (result.firestoreCreated !== null) {
287
+ if (result.firestoreCreated) {
288
+ ui.log.success(tr('new.firestore.created'));
289
+ } else {
290
+ const msg = result.firestoreError
291
+ ? tr('new.firestore.notCreated.error', { error: (result.firestoreError || '').slice(0, 100) })
292
+ : tr('new.firestore.notCreated');
293
+ ui.log.warn(`${msg}\n${tr('new.activateManually')}\n${kleur.cyan(result.firestoreUrl)}`);
294
+ }
291
295
  }
292
296
 
293
- if (result.storageCreated) {
294
- ui.log.success(tr('new.storage.created'));
295
- } else {
296
- const msg = result.storageError
297
- ? tr('new.storage.notCreated.error', { error: (result.storageError || '').slice(0, 100) })
298
- : tr('new.storage.notCreated');
299
- ui.log.warn(`${msg}\n${tr('new.activateManually')}\n${kleur.cyan(result.storageUrl)}`);
297
+ if (result.storageCreated !== null) {
298
+ if (result.storageCreated) {
299
+ ui.log.success(tr('new.storage.created'));
300
+ } else {
301
+ const msg = result.storageError
302
+ ? tr('new.storage.notCreated.error', { error: (result.storageError || '').slice(0, 100) })
303
+ : tr('new.storage.notCreated');
304
+ ui.log.warn(`${msg}\n${tr('new.activateManually')}\n${kleur.cyan(result.storageUrl)}`);
305
+ }
300
306
  }
301
307
  }
302
308
 
@@ -424,9 +430,7 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
424
430
  const envChecks = [...getBaseChecks(), ...getPlatformChecks()];
425
431
  await runChecks(envChecks, tr('new.checks.environment'), {
426
432
  t: tr,
427
- compact: true,
428
- spinnerLabel: tr('new.checks.environment.checking'),
429
- doneLabel: tr('new.checks.environment.done'),
433
+ quiet: true,
430
434
  });
431
435
 
432
436
  printBanner(tr);
@@ -478,9 +482,7 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
478
482
  const backendLabel = tr('setup.checks.backend', { backend }) || ` Checking ${backend} tools…`;
479
483
  backendCheckResults = await runChecks(backendChecks, backendLabel, {
480
484
  t: tr,
481
- compact: true,
482
- spinnerLabel: tr('setup.checks.backend.checking', { backend }) || `Checking ${backend} tools`,
483
- doneLabel: tr('setup.checks.backend.done', { backend }) || `${backend} tools ready`,
485
+ quiet: true,
484
486
  });
485
487
  if (hasRequiredFailures(backendCheckResults)) {
486
488
  const installSteps = [tr('new.checks.installFirebase')];
@@ -604,7 +606,10 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
604
606
  }
605
607
  return true;
606
608
  };
607
- await ensureBilling();
609
+ // Only the Firebase backend uses Firestore / Storage / Cloud Functions, which
610
+ // require Blaze. Supabase and API use Firebase ONLY for FCM/push (free), so we
611
+ // never force a billing account on them.
612
+ if (backend === 'firebase') await ensureBilling();
608
613
  }
609
614
 
610
615
  // Visible section header for Advanced — helps the user track where they are.
@@ -947,17 +952,18 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
947
952
  onCancel: cancel,
948
953
  });
949
954
  } else {
950
- // Warn before the org/billing prompts so the user is ready for the long call.
955
+ // Supabase/API only need the Firebase project for FCM/push (free), so we
956
+ // skip the billing prompt entirely and create in fcmOnly mode (no Blaze,
957
+ // no Firestore/Storage).
951
958
  ui.log.info(tr('new.firebase.create.estimatedTime'));
952
959
  const selectedOrgId = await promptOrganizationIfNeeded(tr, cancel);
953
- const selectedBillingId = await promptBillingAccountIfNeeded(tr, cancel);
954
960
  const ps3 = ui.makeQuickStepper({ color: paintLime });
955
961
  ps3.next(tr('new.firebase.create.creatingPush'));
956
962
  const setupResult = await setupFromScratch(core.appName.trim(), core.bundleId.trim(), {
957
963
  includeWeb: true,
958
964
  region: firebaseRegion,
959
965
  tr,
960
- billingAccountId: selectedBillingId || undefined,
966
+ fcmOnly: true,
961
967
  organizationId: selectedOrgId || undefined,
962
968
  onProgress: (key) => {
963
969
  if (key === 'wait-propagate') {
@@ -905,7 +905,7 @@ async function enableAuthProviders(projectId, { maxRetries = 3, retryDelayMs = 1
905
905
  * @returns {{ ok: boolean, projectId?: string, error?: string, billingFailed?: boolean }}
906
906
  */
907
907
  async function setupFromScratch(appName, bundleId, options = {}) {
908
- const { onProgress = () => {}, includeWeb = true, region = 'us-central1', tr, resumeFromBilling, billingAccountId: preferredBillingId, organizationId } = options;
908
+ const { onProgress = () => {}, includeWeb = true, region = 'us-central1', tr, resumeFromBilling, billingAccountId: preferredBillingId, organizationId, fcmOnly = false } = options;
909
909
  const projectId = resumeFromBilling?.projectId || generateProjectId(appName);
910
910
 
911
911
  const authCheck = await checkGcloudAuth();
@@ -917,34 +917,40 @@ async function setupFromScratch(appName, bundleId, options = {}) {
917
917
  if (!createResult.ok) return { ok: false, error: createResult.error };
918
918
  }
919
919
 
920
- onProgress('billing');
921
- const billingList = await listBillingAccounts();
922
- const hasAccounts = billingList.ok && billingList.accounts?.length > 0;
923
- if (!hasAccounts && !preferredBillingId) {
924
- return {
925
- ok: false,
926
- error: `Project ${projectId} was created but no billing account found. Create one at https://console.cloud.google.com/billing and link it. Then use "Use existing project" with ID: ${projectId}`,
927
- projectId,
928
- };
929
- }
930
- const billingAccountId = preferredBillingId || billingList.accounts[0].id;
931
- const linkResult = await linkBillingAccount(projectId, billingAccountId);
932
- if (!linkResult.ok) {
933
- // Manage URL: remove projects from billing to free quota, then retry
934
- const manageLink = `https://console.cloud.google.com/billing/${billingAccountId}/manage?project=${projectId}`;
935
- const rawError = linkResult.error;
936
- const isQuota = isBillingQuotaError(rawError);
937
- const shortError = isQuota
938
- ? 'Cloud billing quota exceeded (too many projects linked to this billing account)'
939
- : rawError;
940
- return {
941
- ok: false,
942
- error: `Project created but billing link failed: ${shortError}. Manage projects: ${manageLink}`,
943
- projectId,
944
- billingFailed: true,
945
- billingQuotaError: isQuota,
946
- billingManualLink: manageLink,
947
- };
920
+ // Billing (Blaze) is only needed for Firestore / Storage / Cloud Functions,
921
+ // which only the Firebase backend uses. In fcmOnly mode (Supabase/API need a
922
+ // Firebase project purely for FCM/push, which is free) we skip linking a
923
+ // billing account entirely — no Blaze, no credit card.
924
+ if (!fcmOnly) {
925
+ onProgress('billing');
926
+ const billingList = await listBillingAccounts();
927
+ const hasAccounts = billingList.ok && billingList.accounts?.length > 0;
928
+ if (!hasAccounts && !preferredBillingId) {
929
+ return {
930
+ ok: false,
931
+ error: `Project ${projectId} was created but no billing account found. Create one at https://console.cloud.google.com/billing and link it. Then use "Use existing project" with ID: ${projectId}`,
932
+ projectId,
933
+ };
934
+ }
935
+ const billingAccountId = preferredBillingId || billingList.accounts[0].id;
936
+ const linkResult = await linkBillingAccount(projectId, billingAccountId);
937
+ if (!linkResult.ok) {
938
+ // Manage URL: remove projects from billing to free quota, then retry
939
+ const manageLink = `https://console.cloud.google.com/billing/${billingAccountId}/manage?project=${projectId}`;
940
+ const rawError = linkResult.error;
941
+ const isQuota = isBillingQuotaError(rawError);
942
+ const shortError = isQuota
943
+ ? 'Cloud billing quota exceeded (too many projects linked to this billing account)'
944
+ : rawError;
945
+ return {
946
+ ok: false,
947
+ error: `Project created but billing link failed: ${shortError}. Manage projects: ${manageLink}`,
948
+ projectId,
949
+ billingFailed: true,
950
+ billingQuotaError: isQuota,
951
+ billingManualLink: manageLink,
952
+ };
953
+ }
948
954
  }
949
955
 
950
956
  onProgress('enable-apis');
@@ -993,30 +999,38 @@ async function setupFromScratch(appName, bundleId, options = {}) {
993
999
  });
994
1000
  }
995
1001
 
996
- onProgress('firestore');
997
- const firestoreResult = await createFirestoreDatabase(projectId, region);
998
- if (!firestoreResult.ok) {
999
- onProgress('firestore-skipped', {
1000
- error: firestoreResult.error,
1001
- url: `https://console.firebase.google.com/project/${projectId}/firestore`,
1002
- });
1003
- }
1002
+ // Firestore + Storage are Firebase-backend features (and need Blaze). In
1003
+ // fcmOnly mode the app's data/storage live in Supabase or the user's API, so
1004
+ // we don't create them here — that's also what lets us skip billing above.
1005
+ // null = not applicable (the return below reports it as "not created").
1006
+ let firestoreResult = null;
1007
+ let storageResult = null;
1008
+ if (!fcmOnly) {
1009
+ onProgress('firestore');
1010
+ firestoreResult = await createFirestoreDatabase(projectId, region);
1011
+ if (!firestoreResult.ok) {
1012
+ onProgress('firestore-skipped', {
1013
+ error: firestoreResult.error,
1014
+ url: `https://console.firebase.google.com/project/${projectId}/firestore`,
1015
+ });
1016
+ }
1004
1017
 
1005
- onProgress('storage');
1006
- const storageResult = await createFirebaseStorageBucket(projectId, region);
1007
- if (!storageResult.ok) {
1008
- onProgress('storage-skipped', {
1009
- error: storageResult.error,
1010
- url: `https://console.firebase.google.com/project/${projectId}/storage`,
1011
- });
1012
- } else {
1013
- onProgress('storage-cors');
1014
- const corsResult = await applyStorageCors(projectId);
1015
- if (!corsResult.ok) {
1016
- onProgress('storage-cors-warn', {
1017
- error: corsResult.error,
1018
- url: `https://console.cloud.google.com/storage/browser?project=${projectId}`,
1018
+ onProgress('storage');
1019
+ storageResult = await createFirebaseStorageBucket(projectId, region);
1020
+ if (!storageResult.ok) {
1021
+ onProgress('storage-skipped', {
1022
+ error: storageResult.error,
1023
+ url: `https://console.firebase.google.com/project/${projectId}/storage`,
1019
1024
  });
1025
+ } else {
1026
+ onProgress('storage-cors');
1027
+ const corsResult = await applyStorageCors(projectId);
1028
+ if (!corsResult.ok) {
1029
+ onProgress('storage-cors-warn', {
1030
+ error: corsResult.error,
1031
+ url: `https://console.cloud.google.com/storage/browser?project=${projectId}`,
1032
+ });
1033
+ }
1020
1034
  }
1021
1035
  }
1022
1036
 
@@ -1060,11 +1074,11 @@ async function setupFromScratch(appName, bundleId, options = {}) {
1060
1074
  sha1Skipped,
1061
1075
  sha1Error,
1062
1076
  sha1ManualUrl: `https://console.firebase.google.com/project/${projectId}/settings/general/android:${bundleId}`,
1063
- firestoreCreated: firestoreResult.ok,
1064
- firestoreError: firestoreResult.ok ? null : firestoreResult.error,
1077
+ firestoreCreated: firestoreResult ? firestoreResult.ok : null,
1078
+ firestoreError: firestoreResult && !firestoreResult.ok ? firestoreResult.error : null,
1065
1079
  firestoreUrl: `https://console.firebase.google.com/project/${projectId}/firestore`,
1066
- storageCreated: storageResult.ok,
1067
- storageError: storageResult.ok ? null : storageResult.error,
1080
+ storageCreated: storageResult ? storageResult.ok : null,
1081
+ storageError: storageResult && !storageResult.ok ? storageResult.error : null,
1068
1082
  storageUrl: `https://console.firebase.google.com/project/${projectId}/storage`,
1069
1083
  };
1070
1084
  }
@@ -357,29 +357,45 @@ async function runChecks(checks, title, options = {}) {
357
357
  // replaces the single frozen "…" spinner that looked stuck during long
358
358
  // installs (e.g. FlutterFire). Same stepper the kasy-new flow uses, so the
359
359
  // environment setup feels as guided as the project generation, on every OS.
360
- const stepper = ui.makeTimedStepper();
360
+ // quiet mode (used by `kasy new`): don't list tools that are already present —
361
+ // that's the doctor's job. Stay silent on a passing check and ONLY surface a
362
+ // line when a tool actually has to be installed (with its clock). Non-quiet
363
+ // (doctor) shows every step. Either way, failures are handled below.
364
+ const quiet = options.quiet === true;
365
+ const stepper = quiet ? null : ui.makeTimedStepper();
361
366
  const results = [];
362
367
 
363
368
  for (const check of checks) {
364
- stepper.next(t('checks.checking', { name: check.name }));
369
+ if (stepper) stepper.next(t('checks.checking', { name: check.name }));
370
+ let installSpinner = null;
365
371
  const result = await runSingleCheck({ ...check, t }, {
366
372
  showVersion,
367
- // Switch the line to "Installing X…" while the auto-install runs, so the
368
- // clock keeps ticking against a message that explains the wait.
369
- onInstalling: (c) => stepper.update(
370
- c.tryInstallMessageKey ? t(c.tryInstallMessageKey) : t('setup.installingNamed', { name: c.name }),
371
- ),
373
+ // Surface "Installing X… [clock]" while the auto-install runs, so the user
374
+ // knows what the wait is. In quiet mode this is the ONLY thing shown.
375
+ onInstalling: (c) => {
376
+ const msg = c.tryInstallMessageKey ? t(c.tryInstallMessageKey) : t('setup.installingNamed', { name: c.name });
377
+ if (stepper) { stepper.update(msg); return; }
378
+ installSpinner = ui.timedSpinner();
379
+ installSpinner.start(msg);
380
+ },
372
381
  });
373
382
  results.push(result);
374
383
 
375
- if (result.ok) {
376
- stepper.succeed(result.version ? `${result.name} — ${result.version}` : result.name);
377
- } else if (result.required) {
378
- const detail = result.autoInstallFailed ? ` — ${t('checks.install.failed')}` : '';
379
- stepper.fail(`${t('checks.missing', { name: result.name })}${detail}`);
380
- } else {
381
- stepper.warn(t('checks.notFound', { name: result.name }));
384
+ if (stepper) {
385
+ if (result.ok) {
386
+ stepper.succeed(result.version ? `${result.name} ${result.version}` : result.name);
387
+ } else if (result.required) {
388
+ const detail = result.autoInstallFailed ? ` — ${t('checks.install.failed')}` : '';
389
+ stepper.fail(`${t('checks.missing', { name: result.name })}${detail}`);
390
+ } else {
391
+ stepper.warn(t('checks.notFound', { name: result.name }));
392
+ }
393
+ } else if (installSpinner) {
394
+ // quiet mode: we only opened a line because something was installing.
395
+ if (result.ok) installSpinner.stop(result.version ? `${result.name} — ${result.version}` : result.name);
396
+ else installSpinner.error(t('checks.missing', { name: result.name }));
382
397
  }
398
+ // quiet + passed without installing → completely silent (the desired behavior).
383
399
  }
384
400
 
385
401
  const failures = results.filter((r) => !r.ok);
@@ -54,6 +54,9 @@ module.exports = {
54
54
  'cli.command.uninstall.description': 'Uninstall Kasy from this computer',
55
55
  'cli.command.upgrade.running': 'Updating kasy CLI...',
56
56
  'cli.command.upgrade.done': 'kasy updated successfully!',
57
+ 'cli.command.upgrade.current': 'Installed version: {version}',
58
+ 'cli.command.upgrade.now': 'Done! You are now on version {version}',
59
+ 'cli.command.upgrade.already': 'You are already on the latest version ({version})',
57
60
  'cli.command.uninstall.running': 'Uninstalling kasy CLI...',
58
61
  'cli.command.uninstall.done': 'kasy uninstalled. Goodbye!',
59
62
  'new.checks.environment': 'Environment checks',
@@ -54,6 +54,9 @@ module.exports = {
54
54
  'cli.command.uninstall.description': 'Desinstala Kasy de esta computadora',
55
55
  'cli.command.upgrade.running': 'Actualizando la CLI kasy...',
56
56
  'cli.command.upgrade.done': 'kasy actualizado correctamente!',
57
+ 'cli.command.upgrade.current': 'Versión instalada: {version}',
58
+ 'cli.command.upgrade.now': '¡Listo! Ahora estás en la versión {version}',
59
+ 'cli.command.upgrade.already': 'Ya estás en la versión más reciente ({version})',
57
60
  'cli.command.uninstall.running': 'Desinstalando la CLI kasy...',
58
61
  'cli.command.uninstall.done': 'kasy desinstalado. Hasta luego!',
59
62
  'new.checks.environment': 'Verificaciones de entorno',
@@ -54,6 +54,9 @@ module.exports = {
54
54
  'cli.command.uninstall.description': 'Desinstala a Kasy deste computador',
55
55
  'cli.command.upgrade.running': 'Atualizando a CLI kasy...',
56
56
  'cli.command.upgrade.done': 'kasy atualizado com sucesso!',
57
+ 'cli.command.upgrade.current': 'Versão instalada: {version}',
58
+ 'cli.command.upgrade.now': 'Pronto! Agora você está na versão {version}',
59
+ 'cli.command.upgrade.already': 'Você já está na versão mais recente ({version})',
57
60
  'cli.command.uninstall.running': 'Desinstalando a CLI kasy...',
58
61
  'cli.command.uninstall.done': 'kasy desinstalado. Até mais!',
59
62
  'new.checks.environment': 'Verificações de ambiente',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kasy-cli",
3
- "version": "1.21.3",
3
+ "version": "1.21.4",
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"