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 +34 -2
- package/lib/commands/doctor.js +33 -13
- package/lib/commands/new.js +30 -24
- package/lib/scaffold/backends/firebase/setup-from-scratch.js +69 -55
- package/lib/utils/checks.js +65 -14
- package/lib/utils/i18n/messages-en.js +8 -0
- package/lib/utils/i18n/messages-es.js +8 -0
- package/lib/utils/i18n/messages-pt.js +8 -0
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/lib/commands/doctor.js
CHANGED
|
@@ -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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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
|
package/lib/commands/new.js
CHANGED
|
@@ -281,22 +281,28 @@ function printCreateFromScratchStatus(result, tr) {
|
|
|
281
281
|
ui.log.success(tr('new.sha1.added'));
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
const
|
|
936
|
-
const
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
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
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
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
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
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 ?
|
|
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 ?
|
|
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
|
}
|
package/lib/utils/checks.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
368
|
-
//
|
|
369
|
-
onInstalling: (c) =>
|
|
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 (
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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:',
|