kasy-cli 1.21.2 → 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
@@ -436,11 +436,43 @@ function buildProgram(language) {
436
436
  .description(t('cli.command.upgrade.description'))
437
437
  .action(() => {
438
438
  const { spawnSync } = require('node:child_process');
439
+ const path = require('node:path');
440
+ const fs = require('node:fs');
439
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 })));
440
451
  console.log(kleur.cyan(t('cli.command.upgrade.running')) + '\n');
441
- const result = spawnSync('npm', ['install', '-g', 'kasy-cli'], { stdio: 'inherit', shell: true });
452
+ // Update INTO the prefix the CLI actually lives in. The installer uses
453
+ // `npm install -g --prefix ~/.kasy`, so a bare `npm install -g` would
454
+ // land in npm's default global prefix and the old copy on PATH would
455
+ // keep winning. Derive the prefix from this file's own location:
456
+ // unix: <prefix>/lib/node_modules/kasy-cli/bin → drop lib + node_modules
457
+ // win: <prefix>/node_modules/kasy-cli/bin → drop node_modules
458
+ const segs = __dirname.split(path.sep);
459
+ const nm = segs.lastIndexOf('node_modules');
460
+ let prefix = null;
461
+ if (nm > 0) {
462
+ let p = segs.slice(0, nm);
463
+ if (p[p.length - 1] === 'lib') p = p.slice(0, -1);
464
+ prefix = p.join(path.sep) || null;
465
+ }
466
+ const args = ['install', '-g', 'kasy-cli@latest'];
467
+ if (prefix) args.push('--prefix', prefix);
468
+ const result = spawnSync('npm', args, { stdio: 'inherit', shell: true });
442
469
  if (result.status === 0) {
443
- 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');
444
476
  } else {
445
477
  process.exitCode = result.status ?? 1;
446
478
  }
@@ -6,9 +6,10 @@ const { createTranslator, detectDefaultLanguage } = require('../utils/i18n');
6
6
  const {
7
7
  getBaseChecks,
8
8
  getPlatformChecks,
9
- getBackendChecks,
10
9
  runChecks,
11
- hasRequiredFailures
10
+ hasRequiredFailures,
11
+ probeTools,
12
+ BACKEND_TOOLS,
12
13
  } = require('../utils/checks');
13
14
  const {
14
15
  validateAppleSetup,
@@ -21,11 +22,35 @@ const { validateGoogleIosUrlScheme, validateAppleSignInEntitlement, validateFace
21
22
  const { listBillingAccounts, checkGcloudAuth } = require('../scaffold/backends/firebase/setup-from-scratch');
22
23
  const { printCompactHeader } = require('../utils/brand');
23
24
 
24
- function collectOptionalBackendChecks() {
25
- return [
26
- ...getBackendChecks('firebase'),
27
- ...getBackendChecks('supabase')
28
- ];
25
+ /**
26
+ * Read-only backend tools status + a per-backend "what you need" view, so
27
+ * `kasy doctor` answers "what do I have, what's missing, and for which backend".
28
+ */
29
+ async function printBackendReadiness(t) {
30
+ const status = await probeTools();
31
+
32
+ ui.log.step(kleur.bold(t('doctor.tools.title')));
33
+ for (const key of ['firebase', 'flutterfire', 'supabase', 'gcloud']) {
34
+ const s = status[key];
35
+ if (s.ok) {
36
+ ui.log.success(`${s.name}${s.version ? ` — ${s.version}` : ''}`);
37
+ } else if (key === 'gcloud') {
38
+ ui.log.message(kleur.dim(`○ ${s.name} — ${t('doctor.gcloud.note')}`));
39
+ } else {
40
+ ui.log.warn(`${s.name} — ${t('checks.notFound.short')}`);
41
+ }
42
+ }
43
+
44
+ ui.log.step(kleur.bold(t('doctor.byBackend.title')));
45
+ const labels = { firebase: '🔥 Firebase', supabase: '🟢 Supabase', api: '🔗 API REST' };
46
+ for (const [backend, req] of Object.entries(BACKEND_TOOLS)) {
47
+ const missing = req.required.filter((k) => !status[k].ok).map((k) => status[k].name);
48
+ if (missing.length === 0) {
49
+ ui.log.success(`${labels[backend]} — ${t('doctor.backend.ready')}`);
50
+ } else {
51
+ ui.log.warn(`${labels[backend]} — ${t('doctor.backend.missing', { tools: missing.join(', ') })}`);
52
+ }
53
+ }
29
54
  }
30
55
 
31
56
  /**
@@ -242,12 +267,7 @@ async function runDoctor(options = {}) {
242
267
  { t, compact: true, spinnerLabel: t('doctor.baseEnvironment'), doneLabel: t('doctor.baseEnvironment') }
243
268
  );
244
269
 
245
- const optionalBackend = collectOptionalBackendChecks();
246
- if (optionalBackend.length > 0) {
247
- await runChecks(optionalBackend, t('doctor.optionalBackend'), {
248
- t, compact: true, spinnerLabel: t('doctor.optionalBackend'), doneLabel: t('doctor.optionalBackend'),
249
- });
250
- }
270
+ await printBackendReadiness(t);
251
271
 
252
272
  // ── Google Cloud billing account (Firebase Blaze) ─────────────────────
253
273
  // Only meaningful when gcloud is authenticated; otherwise users already saw
@@ -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);
@@ -406,6 +422,39 @@ function hasRequiredFailures(results) {
406
422
  return results.some((result) => result.required && !result.ok);
407
423
  }
408
424
 
425
+ // Backend tools probed by `kasy doctor`, plus which backend needs which. gcloud
426
+ // is listed only as Firebase-optional (create-from-scratch), matching the rule
427
+ // that Supabase/API never need it. Keep in sync with BACKEND_CHECKS above.
428
+ const PROBE_TOOLS = [
429
+ { key: 'firebase', name: 'Firebase CLI', command: 'firebase --version' },
430
+ { key: 'flutterfire', name: 'FlutterFire CLI', command: 'flutterfire --version', pubGlobalBin: 'flutterfire' },
431
+ { key: 'supabase', name: 'Supabase CLI', command: 'supabase --version' },
432
+ { key: 'gcloud', name: 'gcloud', command: 'gcloud --version' },
433
+ ];
434
+
435
+ const BACKEND_TOOLS = {
436
+ firebase: { required: ['firebase', 'flutterfire'], optional: ['gcloud'] },
437
+ supabase: { required: ['firebase', 'flutterfire', 'supabase'], optional: [] },
438
+ api: { required: ['firebase', 'flutterfire'], optional: [] },
439
+ };
440
+
441
+ /**
442
+ * Read-only probe of the backend tools (never installs anything) — for the
443
+ * doctor's "what do I have / what's missing" view. Runs in parallel.
444
+ * @returns {Promise<Object<string, {name, ok, version}>>}
445
+ */
446
+ async function probeTools(tools = PROBE_TOOLS) {
447
+ const entries = await Promise.all(tools.map(async (tool) => {
448
+ const res = await verifyTool(tool);
449
+ return [tool.key, {
450
+ name: tool.name,
451
+ ok: res.ok,
452
+ version: res.ok ? extractVersion(res.stdout, tool.name) : null,
453
+ }];
454
+ }));
455
+ return Object.fromEntries(entries);
456
+ }
457
+
409
458
  module.exports = {
410
459
  getBaseChecks,
411
460
  getPlatformChecks,
@@ -413,4 +462,6 @@ module.exports = {
413
462
  getInstallGuide,
414
463
  runChecks,
415
464
  hasRequiredFailures,
465
+ probeTools,
466
+ BACKEND_TOOLS,
416
467
  };
@@ -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',
@@ -80,6 +83,11 @@ module.exports = {
80
83
  'doctor.title': 'Kasy Doctor',
81
84
  'doctor.baseEnvironment': 'Base environment',
82
85
  'doctor.optionalBackend': 'Optional backend tooling',
86
+ 'doctor.tools.title': 'Backend tools',
87
+ 'doctor.byBackend.title': 'What each backend needs',
88
+ 'doctor.backend.ready': 'all set',
89
+ 'doctor.backend.missing': 'still missing: {tools}',
90
+ 'doctor.gcloud.note': 'optional, only to create a Firebase project from scratch',
83
91
  'doctor.gcpBilling.title': 'Google Cloud Billing (Firebase Blaze)',
84
92
  'doctor.gcpBilling.found': '{count} active billing account(s):',
85
93
  'doctor.gcpBilling.missing': 'No billing account found. Create one before running `kasy new` with Firebase:',
@@ -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',
@@ -80,6 +83,11 @@ module.exports = {
80
83
  'doctor.title': 'Kasy Doctor',
81
84
  'doctor.baseEnvironment': 'Entorno base',
82
85
  'doctor.optionalBackend': 'Herramientas opcionales de backend',
86
+ 'doctor.tools.title': 'Herramientas de backend',
87
+ 'doctor.byBackend.title': 'Qué necesita cada backend',
88
+ 'doctor.backend.ready': 'todo listo',
89
+ 'doctor.backend.missing': 'falta instalar: {tools}',
90
+ 'doctor.gcloud.note': 'opcional, solo para crear un proyecto Firebase desde cero',
83
91
  'doctor.gcpBilling.title': 'Google Cloud Billing (Firebase Blaze)',
84
92
  'doctor.gcpBilling.found': '{count} cuenta(s) de facturación activa(s):',
85
93
  'doctor.gcpBilling.missing': 'No se encontró ninguna cuenta de facturación. Crea una antes de ejecutar `kasy new` con Firebase:',
@@ -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',
@@ -80,6 +83,11 @@ module.exports = {
80
83
  'doctor.title': 'Kasy Doctor',
81
84
  'doctor.baseEnvironment': 'Ambiente base',
82
85
  'doctor.optionalBackend': 'Ferramentas opcionais de backend',
86
+ 'doctor.tools.title': 'Ferramentas de backend',
87
+ 'doctor.byBackend.title': 'O que cada backend precisa',
88
+ 'doctor.backend.ready': 'tudo pronto',
89
+ 'doctor.backend.missing': 'falta instalar: {tools}',
90
+ 'doctor.gcloud.note': 'opcional, só para criar um projeto Firebase do zero',
83
91
  'doctor.gcpBilling.title': 'Google Cloud Billing (Firebase Blaze)',
84
92
  'doctor.gcpBilling.found': '{count} conta(s) de faturamento ativa(s):',
85
93
  'doctor.gcpBilling.missing': 'Nenhuma conta de faturamento encontrada. Crie uma antes de rodar `kasy new` com Firebase:',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kasy-cli",
3
- "version": "1.21.2",
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"