kasy-cli 1.21.3 → 1.21.5
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 +16 -1
- package/lib/commands/new.js +30 -24
- package/lib/scaffold/backends/firebase/setup-from-scratch.js +82 -58
- package/lib/utils/checks.js +30 -14
- package/lib/utils/i18n/messages-en.js +3 -0
- package/lib/utils/i18n/messages-es.js +3 -0
- package/lib/utils/i18n/messages-pt.js +3 -0
- package/package.json +1 -1
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
|
-
|
|
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
|
}
|
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') {
|
|
@@ -49,6 +49,16 @@ const REQUIRED_APIS = [
|
|
|
49
49
|
'logging.googleapis.com', // Cloud Logging (Cloud Build writes build logs here)
|
|
50
50
|
];
|
|
51
51
|
|
|
52
|
+
// fcmOnly (Supabase/API): only the APIs needed for push + Google Sign-In, all
|
|
53
|
+
// of which work on the free Spark plan. Crucially NONE of these require billing
|
|
54
|
+
// — the heavy Cloud Run/Functions/Build/Compute APIs above all demand Blaze.
|
|
55
|
+
const FCM_ONLY_APIS = [
|
|
56
|
+
'firebase.googleapis.com', // Firebase Management (addFirebase)
|
|
57
|
+
'identitytoolkit.googleapis.com', // Firebase Auth / Google Sign-In
|
|
58
|
+
'fcm.googleapis.com', // Cloud Messaging (push) — free
|
|
59
|
+
'iam.googleapis.com', // service accounts (the FCM admin key)
|
|
60
|
+
];
|
|
61
|
+
|
|
52
62
|
async function run(cmd, cwd = process.cwd()) {
|
|
53
63
|
try {
|
|
54
64
|
const { stdout, stderr } = await execAsync(cmd, {
|
|
@@ -382,8 +392,8 @@ async function applyStorageCors(projectId, options = {}) {
|
|
|
382
392
|
/**
|
|
383
393
|
* Enable required Google Cloud APIs.
|
|
384
394
|
*/
|
|
385
|
-
async function enableApis(projectId) {
|
|
386
|
-
const apis = REQUIRED_APIS.join(' ');
|
|
395
|
+
async function enableApis(projectId, fcmOnly = false) {
|
|
396
|
+
const apis = (fcmOnly ? FCM_ONLY_APIS : REQUIRED_APIS).join(' ');
|
|
387
397
|
const result = await run(`gcloud services enable ${apis} --project=${projectId}`);
|
|
388
398
|
if (!result.ok) {
|
|
389
399
|
return { ok: false, error: result.stderr || result.error };
|
|
@@ -905,7 +915,7 @@ async function enableAuthProviders(projectId, { maxRetries = 3, retryDelayMs = 1
|
|
|
905
915
|
* @returns {{ ok: boolean, projectId?: string, error?: string, billingFailed?: boolean }}
|
|
906
916
|
*/
|
|
907
917
|
async function setupFromScratch(appName, bundleId, options = {}) {
|
|
908
|
-
const { onProgress = () => {}, includeWeb = true, region = 'us-central1', tr, resumeFromBilling, billingAccountId: preferredBillingId, organizationId } = options;
|
|
918
|
+
const { onProgress = () => {}, includeWeb = true, region = 'us-central1', tr, resumeFromBilling, billingAccountId: preferredBillingId, organizationId, fcmOnly = false } = options;
|
|
909
919
|
const projectId = resumeFromBilling?.projectId || generateProjectId(appName);
|
|
910
920
|
|
|
911
921
|
const authCheck = await checkGcloudAuth();
|
|
@@ -917,38 +927,44 @@ async function setupFromScratch(appName, bundleId, options = {}) {
|
|
|
917
927
|
if (!createResult.ok) return { ok: false, error: createResult.error };
|
|
918
928
|
}
|
|
919
929
|
|
|
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
|
-
|
|
930
|
+
// Billing (Blaze) is only needed for Firestore / Storage / Cloud Functions,
|
|
931
|
+
// which only the Firebase backend uses. In fcmOnly mode (Supabase/API need a
|
|
932
|
+
// Firebase project purely for FCM/push, which is free) we skip linking a
|
|
933
|
+
// billing account entirely — no Blaze, no credit card.
|
|
934
|
+
if (!fcmOnly) {
|
|
935
|
+
onProgress('billing');
|
|
936
|
+
const billingList = await listBillingAccounts();
|
|
937
|
+
const hasAccounts = billingList.ok && billingList.accounts?.length > 0;
|
|
938
|
+
if (!hasAccounts && !preferredBillingId) {
|
|
939
|
+
return {
|
|
940
|
+
ok: false,
|
|
941
|
+
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}`,
|
|
942
|
+
projectId,
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
const billingAccountId = preferredBillingId || billingList.accounts[0].id;
|
|
946
|
+
const linkResult = await linkBillingAccount(projectId, billingAccountId);
|
|
947
|
+
if (!linkResult.ok) {
|
|
948
|
+
// Manage URL: remove projects from billing to free quota, then retry
|
|
949
|
+
const manageLink = `https://console.cloud.google.com/billing/${billingAccountId}/manage?project=${projectId}`;
|
|
950
|
+
const rawError = linkResult.error;
|
|
951
|
+
const isQuota = isBillingQuotaError(rawError);
|
|
952
|
+
const shortError = isQuota
|
|
953
|
+
? 'Cloud billing quota exceeded (too many projects linked to this billing account)'
|
|
954
|
+
: rawError;
|
|
955
|
+
return {
|
|
956
|
+
ok: false,
|
|
957
|
+
error: `Project created but billing link failed: ${shortError}. Manage projects: ${manageLink}`,
|
|
958
|
+
projectId,
|
|
959
|
+
billingFailed: true,
|
|
960
|
+
billingQuotaError: isQuota,
|
|
961
|
+
billingManualLink: manageLink,
|
|
962
|
+
};
|
|
963
|
+
}
|
|
948
964
|
}
|
|
949
965
|
|
|
950
966
|
onProgress('enable-apis');
|
|
951
|
-
const apisResult = await enableApis(projectId);
|
|
967
|
+
const apisResult = await enableApis(projectId, fcmOnly);
|
|
952
968
|
if (!apisResult.ok) return { ok: false, error: apisResult.error };
|
|
953
969
|
|
|
954
970
|
// Set ADC quota project so Firebase REST API calls (createAndroidApp etc.) work with user credentials.
|
|
@@ -993,30 +1009,38 @@ async function setupFromScratch(appName, bundleId, options = {}) {
|
|
|
993
1009
|
});
|
|
994
1010
|
}
|
|
995
1011
|
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1012
|
+
// Firestore + Storage are Firebase-backend features (and need Blaze). In
|
|
1013
|
+
// fcmOnly mode the app's data/storage live in Supabase or the user's API, so
|
|
1014
|
+
// we don't create them here — that's also what lets us skip billing above.
|
|
1015
|
+
// null = not applicable (the return below reports it as "not created").
|
|
1016
|
+
let firestoreResult = null;
|
|
1017
|
+
let storageResult = null;
|
|
1018
|
+
if (!fcmOnly) {
|
|
1019
|
+
onProgress('firestore');
|
|
1020
|
+
firestoreResult = await createFirestoreDatabase(projectId, region);
|
|
1021
|
+
if (!firestoreResult.ok) {
|
|
1022
|
+
onProgress('firestore-skipped', {
|
|
1023
|
+
error: firestoreResult.error,
|
|
1024
|
+
url: `https://console.firebase.google.com/project/${projectId}/firestore`,
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1004
1027
|
|
|
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}`,
|
|
1028
|
+
onProgress('storage');
|
|
1029
|
+
storageResult = await createFirebaseStorageBucket(projectId, region);
|
|
1030
|
+
if (!storageResult.ok) {
|
|
1031
|
+
onProgress('storage-skipped', {
|
|
1032
|
+
error: storageResult.error,
|
|
1033
|
+
url: `https://console.firebase.google.com/project/${projectId}/storage`,
|
|
1019
1034
|
});
|
|
1035
|
+
} else {
|
|
1036
|
+
onProgress('storage-cors');
|
|
1037
|
+
const corsResult = await applyStorageCors(projectId);
|
|
1038
|
+
if (!corsResult.ok) {
|
|
1039
|
+
onProgress('storage-cors-warn', {
|
|
1040
|
+
error: corsResult.error,
|
|
1041
|
+
url: `https://console.cloud.google.com/storage/browser?project=${projectId}`,
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1020
1044
|
}
|
|
1021
1045
|
}
|
|
1022
1046
|
|
|
@@ -1060,11 +1084,11 @@ async function setupFromScratch(appName, bundleId, options = {}) {
|
|
|
1060
1084
|
sha1Skipped,
|
|
1061
1085
|
sha1Error,
|
|
1062
1086
|
sha1ManualUrl: `https://console.firebase.google.com/project/${projectId}/settings/general/android:${bundleId}`,
|
|
1063
|
-
firestoreCreated: firestoreResult.ok,
|
|
1064
|
-
firestoreError: firestoreResult.ok ?
|
|
1087
|
+
firestoreCreated: firestoreResult ? firestoreResult.ok : null,
|
|
1088
|
+
firestoreError: firestoreResult && !firestoreResult.ok ? firestoreResult.error : null,
|
|
1065
1089
|
firestoreUrl: `https://console.firebase.google.com/project/${projectId}/firestore`,
|
|
1066
|
-
storageCreated: storageResult.ok,
|
|
1067
|
-
storageError: storageResult.ok ?
|
|
1090
|
+
storageCreated: storageResult ? storageResult.ok : null,
|
|
1091
|
+
storageError: storageResult && !storageResult.ok ? storageResult.error : null,
|
|
1068
1092
|
storageUrl: `https://console.firebase.google.com/project/${projectId}/storage`,
|
|
1069
1093
|
};
|
|
1070
1094
|
}
|
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);
|
|
@@ -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',
|