kasy-cli 1.20.1 → 1.21.0-beta.0
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/lib/commands/docs.js +0 -10
- package/lib/commands/ios.js +3 -2
- package/lib/commands/new.js +20 -21
- package/lib/scaffold/CHANGELOG.json +14 -0
- package/lib/scaffold/backends/firebase/setup-from-scratch.js +10 -8
- package/lib/scaffold/shared/post-build.js +11 -6
- package/lib/utils/apple-release.js +1 -10
- package/lib/utils/browser.js +61 -0
- package/lib/utils/checks.js +189 -69
- package/lib/utils/env-tools.js +101 -0
- 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/templates/firebase/lib/components/kasy_sidebar_pro.dart +5 -13
- package/templates/firebase/lib/core/widgets/kasy_hover.dart +9 -1
- package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +1 -0
package/lib/commands/docs.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
const path = require('node:path');
|
|
2
|
-
const { exec } = require('node:child_process');
|
|
3
2
|
const fs = require('fs-extra');
|
|
4
3
|
const kleur = require('kleur');
|
|
5
4
|
const ui = require('../utils/ui');
|
|
@@ -8,15 +7,6 @@ const { createTranslator, detectDefaultLanguage } = require('../utils/i18n');
|
|
|
8
7
|
|
|
9
8
|
const DOCS_FILE = path.join(__dirname, '..', '..', 'docs', 'cli-reference.md');
|
|
10
9
|
|
|
11
|
-
function openInBrowser(url) {
|
|
12
|
-
const cmd = process.platform === 'darwin'
|
|
13
|
-
? `open "${url}"`
|
|
14
|
-
: process.platform === 'win32'
|
|
15
|
-
? `start "" "${url}"`
|
|
16
|
-
: `xdg-open "${url}"`;
|
|
17
|
-
exec(cmd);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
10
|
async function runDocs(options = {}) {
|
|
21
11
|
const t = createTranslator(options.language || detectDefaultLanguage());
|
|
22
12
|
|
package/lib/commands/ios.js
CHANGED
|
@@ -6,6 +6,7 @@ const kleur = require('kleur');
|
|
|
6
6
|
const ui = require('../utils/ui');
|
|
7
7
|
const { printCompactHeader } = require('../utils/brand');
|
|
8
8
|
const { createTranslator, detectDefaultLanguage } = require('../utils/i18n');
|
|
9
|
+
const { homeDir } = require('../utils/env-tools');
|
|
9
10
|
const {
|
|
10
11
|
isKasyFlutterProject,
|
|
11
12
|
readBundleId,
|
|
@@ -73,7 +74,7 @@ async function runConfigure(directory, options = {}) {
|
|
|
73
74
|
message: t('ios.configure.q.p8Path'),
|
|
74
75
|
validate: async (v) => {
|
|
75
76
|
if (!v || !v.trim()) return t('ios.configure.q.required');
|
|
76
|
-
const resolved = path.resolve(v.trim().replace(/^~/,
|
|
77
|
+
const resolved = path.resolve(v.trim().replace(/^~/, homeDir()));
|
|
77
78
|
if (!(await fs.pathExists(resolved))) return t('ios.configure.q.p8NotFound');
|
|
78
79
|
return undefined;
|
|
79
80
|
},
|
|
@@ -87,7 +88,7 @@ async function runConfigure(directory, options = {}) {
|
|
|
87
88
|
|
|
88
89
|
const apiKey = apiKeyRaw.trim();
|
|
89
90
|
const issuerId = issuerIdRaw.trim();
|
|
90
|
-
const p8Source = path.resolve(p8PathRaw.trim().replace(/^~/,
|
|
91
|
+
const p8Source = path.resolve(p8PathRaw.trim().replace(/^~/, homeDir()));
|
|
91
92
|
|
|
92
93
|
const destPath = await installPrivateKey(p8Source, apiKey);
|
|
93
94
|
await writeAppleEnv(projectDir, {
|
package/lib/commands/new.js
CHANGED
|
@@ -19,16 +19,8 @@ function generateWebhookKey() {
|
|
|
19
19
|
return 'rc_wh_' + crypto.randomBytes(16).toString('hex');
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
function openUrl(url) {
|
|
23
|
-
try {
|
|
24
|
-
const { exec } = require('node:child_process');
|
|
25
|
-
const cmd = process.platform === 'darwin' ? `open "${url}"`
|
|
26
|
-
: process.platform === 'win32' ? `start "" "${url}"`
|
|
27
|
-
: `xdg-open "${url}"`;
|
|
28
|
-
exec(cmd, { shell: true });
|
|
29
|
-
} catch (_) {}
|
|
30
|
-
}
|
|
31
22
|
const ui = require('../utils/ui');
|
|
23
|
+
const { openUrl, promptOpenBrowser } = require('../utils/browser');
|
|
32
24
|
const { printBanner, successBox, paintLime } = require('../utils/brand');
|
|
33
25
|
const fs = require('fs-extra');
|
|
34
26
|
const { createTranslator } = require('../utils/i18n');
|
|
@@ -594,8 +586,7 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
594
586
|
if (billing.ok && billing.accounts?.length > 0) return true;
|
|
595
587
|
const billingUrl = 'https://console.cloud.google.com/billing/create';
|
|
596
588
|
ui.log.warn(tr('new.firebase.billing.required'));
|
|
597
|
-
|
|
598
|
-
openUrl(billingUrl);
|
|
589
|
+
await promptOpenBrowser({ url: billingUrl, label: tr('new.firebase.billing.create.steps'), t: tr });
|
|
599
590
|
const ready = await ui.confirm({
|
|
600
591
|
message: tr('new.firebase.billing.created.ready'),
|
|
601
592
|
initialValue: true,
|
|
@@ -815,11 +806,22 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
815
806
|
process.exit(0);
|
|
816
807
|
}
|
|
817
808
|
};
|
|
818
|
-
const showBeforeContinue = (step1Key, authUrl) => {
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
809
|
+
const showBeforeContinue = async (step1Key, authUrl) => {
|
|
810
|
+
// Quick mode keeps prompts to a minimum: show the link and open it
|
|
811
|
+
// automatically. Step-by-step mode uses the Enter-to-open pattern.
|
|
812
|
+
if (isQuick) {
|
|
813
|
+
ui.note(
|
|
814
|
+
`${tr(step1Key)}\n${kleur.cyan(authUrl)}`,
|
|
815
|
+
tr('new.firebase.create.beforeContinue.title')
|
|
816
|
+
);
|
|
817
|
+
openUrl(authUrl);
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
await promptOpenBrowser({
|
|
821
|
+
url: authUrl,
|
|
822
|
+
label: `${tr('new.firebase.create.beforeContinue.title')}\n${tr(step1Key)}`,
|
|
823
|
+
t: tr,
|
|
824
|
+
});
|
|
823
825
|
};
|
|
824
826
|
if (setupResult.ok) {
|
|
825
827
|
ps1.succeed(tr('new.firebase.create.success'));
|
|
@@ -831,8 +833,7 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
831
833
|
// retry will activate it once the OAuth Web Client exists. We only warn
|
|
832
834
|
// here when Email/Anonymous themselves failed (which is the rare path).
|
|
833
835
|
if (!setupResult.authEnabled) {
|
|
834
|
-
showBeforeContinue('new.firebase.create.beforeContinue.step1.noAuth', authUrl);
|
|
835
|
-
openUrl(authUrl);
|
|
836
|
+
await showBeforeContinue('new.firebase.create.beforeContinue.step1.noAuth', authUrl);
|
|
836
837
|
if (!isQuick) {
|
|
837
838
|
await askReady('new.firebase.create.beforeContinue.ready.noAuth');
|
|
838
839
|
}
|
|
@@ -888,8 +889,7 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
888
889
|
printCreateFromScratchStatus(lastResult, tr);
|
|
889
890
|
const authUrl = `https://console.firebase.google.com/project/${core.firebaseProjectId}/authentication/providers`;
|
|
890
891
|
if (!lastResult.authEnabled) {
|
|
891
|
-
showBeforeContinue('new.firebase.create.beforeContinue.step1.noAuth', authUrl);
|
|
892
|
-
openUrl(authUrl);
|
|
892
|
+
await showBeforeContinue('new.firebase.create.beforeContinue.step1.noAuth', authUrl);
|
|
893
893
|
if (!isQuick) {
|
|
894
894
|
await askReady('new.firebase.create.beforeContinue.ready.noAuth');
|
|
895
895
|
}
|
|
@@ -1791,7 +1791,6 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
1791
1791
|
ui.log.warn(
|
|
1792
1792
|
`${tr('new.sha1.failed', { error: (sha1Result.sha1Error || '').slice(0, 120) })}\n${tr('new.sha1.manual')}\n${kleur.cyan(sha1ManualUrl)}`
|
|
1793
1793
|
);
|
|
1794
|
-
openUrl(sha1ManualUrl);
|
|
1795
1794
|
}
|
|
1796
1795
|
}
|
|
1797
1796
|
}
|
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
{
|
|
2
|
+
"1.21.0": {
|
|
3
|
+
"modules": {
|
|
4
|
+
"components": {
|
|
5
|
+
"pt": "KasySidebarPro com renderização mais leve: removidos cálculos de borda redundantes, deixando a sidebar mais simples e performática.",
|
|
6
|
+
"en": "Lighter KasySidebarPro rendering: removed redundant border-radius calculations, making the sidebar simpler and more performant.",
|
|
7
|
+
"es": "Renderizado más ligero de KasySidebarPro: se quitaron cálculos de borde redundantes, dejando la barra lateral más simple y eficiente."
|
|
8
|
+
},
|
|
9
|
+
"core": {
|
|
10
|
+
"pt": "KasyHover agora aceita hoverEnabled: permite desligar o realce ao passar o mouse em listas simples (como a tela de Configurações), mantendo o cursor de clique e o feedback de toque.",
|
|
11
|
+
"en": "KasyHover now accepts hoverEnabled: lets you turn off the hover highlight on plain list rows (like the Settings screen) while keeping the click cursor and press feedback.",
|
|
12
|
+
"es": "KasyHover ahora acepta hoverEnabled: permite desactivar el resaltado al pasar el cursor en listas simples (como la pantalla de Ajustes), manteniendo el cursor de clic y el feedback al pulsar."
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
},
|
|
2
16
|
"1.18.0": {
|
|
3
17
|
"modules": {
|
|
4
18
|
"components": {
|
|
@@ -18,6 +18,7 @@ const { promisify } = require('node:util');
|
|
|
18
18
|
const path = require('node:path');
|
|
19
19
|
const fs = require('fs-extra');
|
|
20
20
|
const os = require('node:os');
|
|
21
|
+
const { getInstallGuide } = require('../../../utils/checks');
|
|
21
22
|
|
|
22
23
|
const execAsync = promisify(exec);
|
|
23
24
|
|
|
@@ -104,14 +105,15 @@ async function checkGcloudAuth() {
|
|
|
104
105
|
* Get platform-specific install instructions for gcloud CLI.
|
|
105
106
|
*/
|
|
106
107
|
function getGcloudInstallInstructions() {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
108
|
+
// Delegates to the single source of truth in utils/checks so the install
|
|
109
|
+
// command shown here can never drift from the one the env checks suggest.
|
|
110
|
+
const guide = getInstallGuide('gcloud');
|
|
111
|
+
return {
|
|
112
|
+
install: guide.cmd,
|
|
113
|
+
hint: guide.cmd ? null : 'Download and run the installer from the link below, then restart the terminal.',
|
|
114
|
+
after: guide.after,
|
|
115
|
+
url: guide.url,
|
|
116
|
+
};
|
|
115
117
|
}
|
|
116
118
|
|
|
117
119
|
/**
|
|
@@ -13,13 +13,17 @@ const { exec } = require('node:child_process');
|
|
|
13
13
|
const { promisify } = require('node:util');
|
|
14
14
|
const path = require('node:path');
|
|
15
15
|
const fs = require('fs-extra');
|
|
16
|
+
const { augmentedEnv, pubCacheBin } = require('../../utils/env-tools');
|
|
16
17
|
|
|
17
18
|
const execAsync = promisify(exec);
|
|
18
19
|
const MAX_BUFFER = 100 * 1024 * 1024; // 100 MB — build_runner can be verbose
|
|
19
20
|
|
|
20
21
|
async function run(cmd, cwd, timeout) {
|
|
21
22
|
try {
|
|
22
|
-
|
|
23
|
+
// Run with the augmented PATH so tools installed earlier in this same
|
|
24
|
+
// session (flutterfire via pub global, etc.) are found without the user
|
|
25
|
+
// having to reopen the terminal — see utils/env-tools.
|
|
26
|
+
const opts = { cwd, maxBuffer: MAX_BUFFER, env: augmentedEnv() };
|
|
23
27
|
if (timeout) opts.timeout = timeout;
|
|
24
28
|
const { stdout, stderr } = await execAsync(cmd, opts);
|
|
25
29
|
return { ok: true, stdout, stderr };
|
|
@@ -62,12 +66,13 @@ async function flutterfireConfigure(projectDir, firebaseProjectId, options = {})
|
|
|
62
66
|
'--yes',
|
|
63
67
|
].join(' ');
|
|
64
68
|
|
|
65
|
-
// Prefer the binary on PATH
|
|
66
|
-
//
|
|
69
|
+
// Prefer the binary on PATH (the augmented PATH in run() already exposes the
|
|
70
|
+
// pub-cache bin dir); fall back to the absolute pub-cache path for shells
|
|
71
|
+
// that still don't see it. pubCacheBin() is cross-platform — it resolves the
|
|
72
|
+
// right place on Windows (%LOCALAPPDATA%\Pub\Cache\bin) vs ~/.pub-cache/bin.
|
|
67
73
|
const result = await run(`flutterfire configure ${args}`, projectDir, 300_000);
|
|
68
|
-
if (!result.ok && (result.error?.includes('ENOENT') || result.error?.includes('not found'))) {
|
|
69
|
-
|
|
70
|
-
return run(`${pubCacheBin} configure ${args}`, projectDir, 300_000);
|
|
74
|
+
if (!result.ok && (result.error?.includes('ENOENT') || result.error?.includes('not found') || result.error?.includes('not recognized'))) {
|
|
75
|
+
return run(`${pubCacheBin('flutterfire')} configure ${args}`, projectDir, 300_000);
|
|
71
76
|
}
|
|
72
77
|
return result;
|
|
73
78
|
}
|
|
@@ -5,6 +5,7 @@ const fs = require('fs-extra');
|
|
|
5
5
|
const os = require('node:os');
|
|
6
6
|
const kleur = require('kleur');
|
|
7
7
|
const ui = require('./ui');
|
|
8
|
+
const { openUrl } = require('./browser');
|
|
8
9
|
const { exec, spawn } = require('node:child_process');
|
|
9
10
|
const { promisify } = require('node:util');
|
|
10
11
|
|
|
@@ -144,16 +145,6 @@ async function installPrivateKey(sourcePath, keyId) {
|
|
|
144
145
|
return destPath;
|
|
145
146
|
}
|
|
146
147
|
|
|
147
|
-
function openUrl(url) {
|
|
148
|
-
const platform = process.platform;
|
|
149
|
-
const cmd =
|
|
150
|
-
platform === 'darwin'
|
|
151
|
-
? `open "${url}"`
|
|
152
|
-
: platform === 'win32'
|
|
153
|
-
? `start "" "${url}"`
|
|
154
|
-
: `xdg-open "${url}"`;
|
|
155
|
-
exec(cmd, () => {});
|
|
156
|
-
}
|
|
157
148
|
|
|
158
149
|
function iosReleaseDocPath() {
|
|
159
150
|
return 'docs/ios-release.md';
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser helpers. Centralizes the platform-specific "open a URL" command that
|
|
3
|
+
* was duplicated across new.js / docs.js / apple-release.js, and adds the
|
|
4
|
+
* standard interaction the CLI uses for any external link:
|
|
5
|
+
*
|
|
6
|
+
* 1. always PRINT the URL (so it works over SSH / headless / no GUI), then
|
|
7
|
+
* 2. let the user press Enter to OPEN it in the default browser.
|
|
8
|
+
*
|
|
9
|
+
* This beats auto-opening (intrusive, fires at the wrong moment) and beats
|
|
10
|
+
* printing a bare URL (the user has to copy-paste). Press Enter and it opens.
|
|
11
|
+
*
|
|
12
|
+
* @module utils/browser
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const { exec } = require('node:child_process');
|
|
16
|
+
const kleur = require('kleur');
|
|
17
|
+
const ui = require('./ui');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Open a URL in the default browser. Best-effort: never throws, and async
|
|
21
|
+
* failures are swallowed because the URL is always printed alongside.
|
|
22
|
+
*
|
|
23
|
+
* @returns {boolean} true if the open command was dispatched
|
|
24
|
+
*/
|
|
25
|
+
function openUrl(url) {
|
|
26
|
+
try {
|
|
27
|
+
const cmd = process.platform === 'darwin'
|
|
28
|
+
? `open "${url}"`
|
|
29
|
+
: process.platform === 'win32'
|
|
30
|
+
? `start "" "${url}"`
|
|
31
|
+
: `xdg-open "${url}"`;
|
|
32
|
+
exec(cmd, () => {}); // swallow async errors — opening a browser is best-effort
|
|
33
|
+
return true;
|
|
34
|
+
} catch (_) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Print a link and offer to open it in the browser on Enter.
|
|
41
|
+
*
|
|
42
|
+
* @param {object} opts
|
|
43
|
+
* @param {string} opts.url the link to show/open
|
|
44
|
+
* @param {string} [opts.label] line shown above the URL
|
|
45
|
+
* @param {string} [opts.confirmMessage] the yes/no prompt (Enter = yes)
|
|
46
|
+
* @param {Function}[opts.t] translator; falls back to English-ish keys
|
|
47
|
+
* @returns {Promise<boolean>} whether the user chose to open it
|
|
48
|
+
*/
|
|
49
|
+
async function promptOpenBrowser({ url, label, confirmMessage, t }) {
|
|
50
|
+
const tr = typeof t === 'function' ? t : null;
|
|
51
|
+
const intro = label || (tr ? tr('browser.open.intro') : 'Open this link in your browser:');
|
|
52
|
+
ui.log.message(`${intro}\n${kleur.cyan(url)}`);
|
|
53
|
+
const open = await ui.confirm({
|
|
54
|
+
message: confirmMessage || (tr ? tr('browser.open.confirm') : 'Open it in the browser now?'),
|
|
55
|
+
initialValue: true,
|
|
56
|
+
});
|
|
57
|
+
if (open) openUrl(url);
|
|
58
|
+
return open;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = { openUrl, promptOpenBrowser };
|
package/lib/utils/checks.js
CHANGED
|
@@ -3,6 +3,8 @@ const { promisify } = require('node:util');
|
|
|
3
3
|
const kleur = require('kleur');
|
|
4
4
|
const ui = require('./ui');
|
|
5
5
|
const { createTranslator, detectDefaultLanguage } = require('./i18n');
|
|
6
|
+
const { augmentedEnv, pubCacheBin } = require('./env-tools');
|
|
7
|
+
const { promptOpenBrowser } = require('./browser');
|
|
6
8
|
|
|
7
9
|
const execAsync = promisify(exec);
|
|
8
10
|
|
|
@@ -10,12 +12,41 @@ const execAsync = promisify(exec);
|
|
|
10
12
|
const TOOL_CHECK_TIMEOUT = 15_000;
|
|
11
13
|
// Timeout para instalação automática de ferramentas (5 min)
|
|
12
14
|
const INSTALL_TIMEOUT = 300_000;
|
|
15
|
+
const MAX_BUFFER = 50 * 1024 * 1024;
|
|
13
16
|
|
|
14
17
|
// Minimum supported versions for Kasy projects
|
|
15
18
|
const MIN_NODE_VERSION = '18.0.0';
|
|
16
19
|
const MIN_FLUTTER_VERSION = '3.24.0';
|
|
17
20
|
const MIN_DART_VERSION = '3.5.0';
|
|
18
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Single source of truth for "how do I install tool X on this OS". Returns the
|
|
24
|
+
* package-manager command we can run (with the user's consent) plus a docs URL
|
|
25
|
+
* and any follow-up command. Used both for the auto-install offer and for the
|
|
26
|
+
* manual guidance fallback, so the two can never drift apart again.
|
|
27
|
+
*
|
|
28
|
+
* @param {'gcloud'|'flutter'} tool
|
|
29
|
+
* @returns {{ cmd: string|null, after: string|null, url: string }}
|
|
30
|
+
*/
|
|
31
|
+
function getInstallGuide(tool) {
|
|
32
|
+
const guides = {
|
|
33
|
+
gcloud: {
|
|
34
|
+
darwin: { cmd: 'brew install --cask google-cloud-sdk', after: 'gcloud auth login', url: 'https://cloud.google.com/sdk/docs/install' },
|
|
35
|
+
win32: { cmd: 'winget install --id Google.CloudSDK -e', after: 'gcloud auth login', url: 'https://cloud.google.com/sdk/docs/install-sdk#windows' },
|
|
36
|
+
linux: { cmd: 'curl https://sdk.cloud.google.com | bash', after: 'gcloud auth login', url: 'https://cloud.google.com/sdk/docs/install' },
|
|
37
|
+
},
|
|
38
|
+
flutter: {
|
|
39
|
+
// Flutter installs via brew cask on macOS; on Windows/Linux the supported
|
|
40
|
+
// path is the SDK archive, so we guide to the docs instead of guessing.
|
|
41
|
+
darwin: { cmd: 'brew install --cask flutter', after: 'flutter doctor', url: 'https://docs.flutter.dev/get-started/install/macos' },
|
|
42
|
+
win32: { cmd: null, after: 'flutter doctor', url: 'https://docs.flutter.dev/get-started/install/windows' },
|
|
43
|
+
linux: { cmd: null, after: 'flutter doctor', url: 'https://docs.flutter.dev/get-started/install/linux' },
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
const byTool = guides[tool] || {};
|
|
47
|
+
return byTool[process.platform] || byTool.linux || { cmd: null, after: null, url: '' };
|
|
48
|
+
}
|
|
49
|
+
|
|
19
50
|
const BASE_CHECKS = [
|
|
20
51
|
{
|
|
21
52
|
name: 'Node.js',
|
|
@@ -29,6 +60,8 @@ const BASE_CHECKS = [
|
|
|
29
60
|
required: false,
|
|
30
61
|
warnMessageKey: 'checks.flutter.warn',
|
|
31
62
|
minVersion: MIN_FLUTTER_VERSION,
|
|
63
|
+
installGuide: () => getInstallGuide('flutter'),
|
|
64
|
+
confirmInstall: true,
|
|
32
65
|
},
|
|
33
66
|
{
|
|
34
67
|
name: 'Dart SDK',
|
|
@@ -55,19 +88,6 @@ const PLATFORM_CHECKS = {
|
|
|
55
88
|
linux: []
|
|
56
89
|
};
|
|
57
90
|
|
|
58
|
-
function getGcloudInstallHint() {
|
|
59
|
-
switch (process.platform) {
|
|
60
|
-
case 'darwin':
|
|
61
|
-
return 'brew install --cask google-cloud-sdk';
|
|
62
|
-
case 'linux':
|
|
63
|
-
return 'curl https://sdk.cloud.google.com | bash && source ~/.bashrc';
|
|
64
|
-
case 'win32':
|
|
65
|
-
return 'winget install Google.CloudSDK\n ou baixe o instalador: https://cloud.google.com/sdk/docs/install-sdk#windows';
|
|
66
|
-
default:
|
|
67
|
-
return 'https://cloud.google.com/sdk/docs/install';
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
91
|
/** Firebase CLI + FlutterFire — required for push notifications (FCM) on all backends */
|
|
72
92
|
const FIREBASE_CHECKS = [
|
|
73
93
|
{
|
|
@@ -82,14 +102,19 @@ const FIREBASE_CHECKS = [
|
|
|
82
102
|
command: 'flutterfire --version',
|
|
83
103
|
required: true,
|
|
84
104
|
tryInstall: 'dart pub global activate flutterfire_cli',
|
|
85
|
-
|
|
105
|
+
// After activation the binary lives in the pub-cache bin dir, which is NOT
|
|
106
|
+
// on the current process PATH (and is in a different place on Windows). We
|
|
107
|
+
// re-check via the augmented PATH first and the absolute pub-cache path as
|
|
108
|
+
// a fallback — see env-tools.pubCacheBin.
|
|
109
|
+
pubGlobalBin: 'flutterfire',
|
|
86
110
|
tryInstallMessageKey: 'setup.flutterfire.installing',
|
|
87
111
|
},
|
|
88
112
|
{
|
|
89
113
|
name: 'gcloud CLI (create-from-scratch)',
|
|
90
114
|
command: 'gcloud --version',
|
|
91
115
|
required: false,
|
|
92
|
-
|
|
116
|
+
installGuide: () => getInstallGuide('gcloud'),
|
|
117
|
+
confirmInstall: true,
|
|
93
118
|
waitPromptKey: 'checks.waitPrompt.gcloud.install',
|
|
94
119
|
},
|
|
95
120
|
{
|
|
@@ -159,61 +184,171 @@ function extractVersion(stdout, checkName) {
|
|
|
159
184
|
return m ? m[0] : raw.slice(0, 20);
|
|
160
185
|
}
|
|
161
186
|
|
|
187
|
+
/** execAsync that never throws and always runs with the augmented PATH. */
|
|
188
|
+
async function execTool(cmd, timeout = TOOL_CHECK_TIMEOUT) {
|
|
189
|
+
try {
|
|
190
|
+
const { stdout, stderr } = await execAsync(cmd, {
|
|
191
|
+
encoding: 'utf8',
|
|
192
|
+
timeout,
|
|
193
|
+
env: augmentedEnv(),
|
|
194
|
+
maxBuffer: MAX_BUFFER,
|
|
195
|
+
});
|
|
196
|
+
return { ok: true, stdout, stderr };
|
|
197
|
+
} catch (err) {
|
|
198
|
+
return { ok: false, error: err.message, stderr: err.stderr || '', stdout: err.stdout || '' };
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Verify a tool responds after an install. Tries the plain command first (now
|
|
204
|
+
* that PATH is augmented) and, for pub-global tools, the absolute pub-cache
|
|
205
|
+
* path as a fallback for shells that don't pick up the new PATH.
|
|
206
|
+
*/
|
|
207
|
+
async function verifyTool(check) {
|
|
208
|
+
const attempts = [check.command];
|
|
209
|
+
if (check.pubGlobalBin) attempts.push(`${pubCacheBin(check.pubGlobalBin)} --version`);
|
|
210
|
+
else if (check.retryCommand) attempts.push(check.retryCommand);
|
|
211
|
+
|
|
212
|
+
for (const cmd of attempts) {
|
|
213
|
+
const res = await execTool(cmd);
|
|
214
|
+
if (res.ok) return { ok: true, stdout: res.stdout };
|
|
215
|
+
}
|
|
216
|
+
return { ok: false };
|
|
217
|
+
}
|
|
218
|
+
|
|
162
219
|
async function runSingleCheck(check, options = {}) {
|
|
163
220
|
const showVersion = check.showVersion !== undefined ? check.showVersion : (options.showVersion !== undefined ? options.showVersion : true);
|
|
164
221
|
let autoInstallFailed = false;
|
|
165
222
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const version = showVersion ? extractVersion(stdout, check.name) : null;
|
|
223
|
+
const first = await execTool(check.command);
|
|
224
|
+
if (first.ok) {
|
|
225
|
+
const version = showVersion ? extractVersion(first.stdout, check.name) : null;
|
|
169
226
|
return { ...check, ok: true, version: version || null };
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const diagnosis = diagnoseFailure({ message: first.error, stderr: first.stderr, stdout: first.stdout });
|
|
230
|
+
|
|
231
|
+
// Lightweight auto-install (npm / pub global). Heavy tools (gcloud, flutter)
|
|
232
|
+
// use confirmInstall and are handled interactively after the spinner.
|
|
233
|
+
if (check.tryInstall) {
|
|
234
|
+
const installed = await execTool(check.tryInstall, INSTALL_TIMEOUT);
|
|
235
|
+
if (installed.ok) {
|
|
236
|
+
const verified = await verifyTool(check);
|
|
237
|
+
if (verified.ok) {
|
|
238
|
+
const version = showVersion ? extractVersion(verified.stdout, check.name) : null;
|
|
177
239
|
return { ...check, ok: true, version: version || null };
|
|
178
|
-
} catch {
|
|
179
|
-
autoInstallFailed = true;
|
|
180
240
|
}
|
|
181
241
|
}
|
|
182
|
-
|
|
242
|
+
autoInstallFailed = true;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return { ...check, ok: false, autoInstallFailed, diagnosis };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Re-run a check's command (with augmented PATH) and report the result inline. */
|
|
249
|
+
async function revalidate(check, t) {
|
|
250
|
+
const verified = await verifyTool(check);
|
|
251
|
+
if (verified.ok) {
|
|
252
|
+
const version = check.showVersion === false ? null : extractVersion(verified.stdout, check.name);
|
|
253
|
+
ui.log.success(version ? `${check.name} — ${version}` : check.name);
|
|
254
|
+
return true;
|
|
183
255
|
}
|
|
256
|
+
ui.log.error(`${check.name} ${t('checks.stillMissing') || 'still missing'}`);
|
|
257
|
+
return false;
|
|
184
258
|
}
|
|
185
259
|
|
|
186
260
|
/**
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
261
|
+
* Interactive recovery for a failed check. The flow, in order:
|
|
262
|
+
* 1. Heavy tools with a package-manager command (gcloud, flutter): offer to
|
|
263
|
+
* install it now via brew/winget after a yes/no confirm.
|
|
264
|
+
* 2. Manual guidance: print the exact command for this OS, offer to open the
|
|
265
|
+
* docs in the browser on Enter.
|
|
266
|
+
* 3. Wait for the user to fix it, then re-check on Enter (never a dead end).
|
|
267
|
+
* Returns true if the tool is present by the end.
|
|
190
268
|
*/
|
|
191
|
-
async function
|
|
269
|
+
async function recoverCheckInteractively(check, t) {
|
|
192
270
|
ui.log.warn(`${check.name} ${t('checks.notFound.short') || 'not found'}`);
|
|
193
|
-
|
|
194
|
-
|
|
271
|
+
|
|
272
|
+
const guide = typeof check.installGuide === 'function' ? check.installGuide() : null;
|
|
273
|
+
|
|
274
|
+
// 1) Offer auto-install for heavy tools that have a package-manager command.
|
|
275
|
+
if (guide && guide.cmd && check.confirmInstall) {
|
|
276
|
+
const doInstall = await ui.confirm({
|
|
277
|
+
message: t('checks.install.confirm', { name: check.name, cmd: guide.cmd }),
|
|
278
|
+
initialValue: true,
|
|
279
|
+
});
|
|
280
|
+
if (doInstall) {
|
|
281
|
+
const spinner = ui.spinner();
|
|
282
|
+
spinner.start(t('checks.install.running', { name: check.name }));
|
|
283
|
+
const installed = await execTool(guide.cmd, INSTALL_TIMEOUT);
|
|
284
|
+
spinner.stop(t('checks.install.running', { name: check.name }));
|
|
285
|
+
if (installed.ok && (await revalidate(check, t))) return true;
|
|
286
|
+
ui.log.warn(t('checks.install.failedManual', { name: check.name }));
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// 2) Manual guidance — the exact command for this OS + an Enter-to-open link.
|
|
291
|
+
// Fall back to the auto-install command (e.g. the `dart pub global activate`
|
|
292
|
+
// that just failed) so the user always sees *how* to install it by hand,
|
|
293
|
+
// running it in their own shell where the env may differ.
|
|
294
|
+
const manualCmd = (guide && guide.cmd) || check.tryInstall || check.failHint;
|
|
295
|
+
if (manualCmd) {
|
|
296
|
+
ui.log.message(`${t('checks.runHint') || 'Run'}: ${kleur.cyan(manualCmd)}`);
|
|
297
|
+
}
|
|
298
|
+
if (guide && guide.after) {
|
|
299
|
+
ui.log.message(`${t('checks.thenRun') || 'Then run'}: ${kleur.cyan(guide.after)}`);
|
|
195
300
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
301
|
+
if (guide && guide.url) {
|
|
302
|
+
await promptOpenBrowser({
|
|
303
|
+
url: guide.url,
|
|
304
|
+
t,
|
|
305
|
+
label: t('checks.install.openDocs', { name: check.name }),
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// 3) Wait + re-check on Enter.
|
|
310
|
+
const waitMessage = check.waitPromptKey
|
|
311
|
+
? t(check.waitPromptKey)
|
|
312
|
+
: (check.waitPrompt || t('checks.recheck', { name: check.name }));
|
|
313
|
+
const proceed = await ui.confirm({ message: waitMessage, initialValue: true });
|
|
200
314
|
if (!proceed) return false;
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
|
|
315
|
+
return revalidate(check, t);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** Non-interactive failure reporting (CI, doctor without a TTY, or user opt-out). */
|
|
319
|
+
function printCheckFailure(result, t) {
|
|
320
|
+
const guide = typeof result.installGuide === 'function' ? result.installGuide() : null;
|
|
321
|
+
const cmdHint = (guide && guide.cmd) || result.tryInstall || result.failHint;
|
|
322
|
+
const hint = cmdHint ? `\n${kleur.dim(`→ ${cmdHint}`)}` : '';
|
|
323
|
+
const urlHint = guide && guide.url ? `\n${kleur.dim(`→ ${guide.url}`)}` : '';
|
|
324
|
+
|
|
325
|
+
if (result.required) {
|
|
326
|
+
const detail = result.autoInstallFailed ? ` — ${t('checks.install.failed')}` : '';
|
|
327
|
+
const diagSuffix = result.diagnosis ? `\n${kleur.dim(`→ ${t(`checks.diagnostic.${result.diagnosis}`, { name: result.name })}`)}` : '';
|
|
328
|
+
ui.log.error(`${t('checks.missing', { name: result.name })}${detail}${diagSuffix}${hint}${urlHint}`);
|
|
329
|
+
} else if (result.diagnosis) {
|
|
330
|
+
ui.log.warn(t(`checks.diagnostic.${result.diagnosis}`, { name: result.name }));
|
|
331
|
+
} else if (result.warnMessage) {
|
|
332
|
+
ui.log.warn(`${result.warnMessage}${hint}`);
|
|
333
|
+
} else if (result.warnMessageKey) {
|
|
334
|
+
ui.log.warn(`${t(result.warnMessageKey)}${hint}`);
|
|
335
|
+
} else {
|
|
336
|
+
ui.log.warn(`${t('checks.notFound', { name: result.name })}${hint}${urlHint}`);
|
|
211
337
|
}
|
|
212
338
|
}
|
|
213
339
|
|
|
340
|
+
function canRecover(result) {
|
|
341
|
+
return Boolean(
|
|
342
|
+
result.installGuide || result.tryInstall || result.waitPromptKey || result.waitPrompt
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
214
346
|
async function runChecks(checks, title, options = {}) {
|
|
215
347
|
const t = options.t || createTranslator(options.language || detectDefaultLanguage());
|
|
216
348
|
const { showVersion = true } = options;
|
|
349
|
+
// Interactive recovery needs a real terminal; disable it in CI/pipes so the
|
|
350
|
+
// command never blocks waiting for input that will never come.
|
|
351
|
+
const interactive = options.interactive !== false && Boolean(process.stdout.isTTY);
|
|
217
352
|
|
|
218
353
|
// Single spinner over all checks, show failures afterwards. The visual
|
|
219
354
|
// sits inside the clack rail (│) opened by the caller's ui.intro().
|
|
@@ -243,31 +378,15 @@ async function runChecks(checks, title, options = {}) {
|
|
|
243
378
|
}
|
|
244
379
|
|
|
245
380
|
for (const result of failures) {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
const waitPromptText = result.waitPromptKey ? t(result.waitPromptKey) : result.waitPrompt;
|
|
249
|
-
if (waitPromptText) {
|
|
250
|
-
const recovered = await retryCheckInteractively({ ...result, waitPrompt: waitPromptText }, t);
|
|
381
|
+
if (interactive && canRecover(result)) {
|
|
382
|
+
const recovered = await recoverCheckInteractively({ ...result, t }, t);
|
|
251
383
|
if (recovered) {
|
|
252
384
|
const idx = results.indexOf(result);
|
|
253
|
-
if (idx >= 0) results[idx] = { ...result, ok: true };
|
|
385
|
+
if (idx >= 0) results[idx] = { ...result, ok: true, autoInstallFailed: false };
|
|
386
|
+
continue;
|
|
254
387
|
}
|
|
255
|
-
continue;
|
|
256
|
-
}
|
|
257
|
-
const hint = result.failHint ? `\n${kleur.dim(`→ ${result.failHint}`)}` : '';
|
|
258
|
-
if (result.required) {
|
|
259
|
-
const detail = result.autoInstallFailed ? ` — ${t('checks.install.failed')}` : '';
|
|
260
|
-
const diagSuffix = result.diagnosis ? `\n${kleur.dim(`→ ${t(`checks.diagnostic.${result.diagnosis}`, { name: result.name })}`)}` : '';
|
|
261
|
-
ui.log.error(`${t('checks.missing', { name: result.name })}${detail}${diagSuffix}${hint}`);
|
|
262
|
-
} else if (result.diagnosis) {
|
|
263
|
-
ui.log.warn(t(`checks.diagnostic.${result.diagnosis}`, { name: result.name }));
|
|
264
|
-
} else if (result.warnMessage) {
|
|
265
|
-
ui.log.warn(`${result.warnMessage}${hint}`);
|
|
266
|
-
} else if (result.warnMessageKey) {
|
|
267
|
-
ui.log.warn(`${t(result.warnMessageKey)}${hint}`);
|
|
268
|
-
} else {
|
|
269
|
-
ui.log.warn(`${t('checks.notFound', { name: result.name })}${hint}`);
|
|
270
388
|
}
|
|
389
|
+
printCheckFailure(result, t);
|
|
271
390
|
}
|
|
272
391
|
|
|
273
392
|
return results;
|
|
@@ -281,6 +400,7 @@ module.exports = {
|
|
|
281
400
|
getBaseChecks,
|
|
282
401
|
getPlatformChecks,
|
|
283
402
|
getBackendChecks,
|
|
403
|
+
getInstallGuide,
|
|
284
404
|
runChecks,
|
|
285
405
|
hasRequiredFailures,
|
|
286
406
|
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-platform resolution of tool install locations and an "augmented" PATH.
|
|
3
|
+
*
|
|
4
|
+
* Why this exists: several places in the CLI used `process.env.HOME` to build
|
|
5
|
+
* the pub-cache path (e.g. `${HOME}/.pub-cache/bin/flutterfire`). On Windows
|
|
6
|
+
* `HOME` is undefined (it's `USERPROFILE`) AND the pub-cache lives somewhere
|
|
7
|
+
* else entirely (`%LOCALAPPDATA%\Pub\Cache\bin`), so the fallback never worked
|
|
8
|
+
* there — which is why FlutterFire "never installed" on Windows.
|
|
9
|
+
*
|
|
10
|
+
* It also exposes `augmentedEnv()`: a copy of the environment with the dirs of
|
|
11
|
+
* freshly-installed CLIs prepended to PATH. A running process inherits the PATH
|
|
12
|
+
* of the shell that launched it and never sees changes made afterwards, so a
|
|
13
|
+
* tool installed mid-`kasy new` (flutterfire, firebase…) is invisible to child
|
|
14
|
+
* processes until the user opens a new terminal — unless we inject the dir here.
|
|
15
|
+
*
|
|
16
|
+
* @module utils/env-tools
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const os = require('node:os');
|
|
20
|
+
const path = require('node:path');
|
|
21
|
+
const fs = require('node:fs');
|
|
22
|
+
|
|
23
|
+
const isWindows = process.platform === 'win32';
|
|
24
|
+
const PATH_SEP = isWindows ? ';' : ':';
|
|
25
|
+
|
|
26
|
+
/** Cross-platform home directory (never trust process.env.HOME on Windows). */
|
|
27
|
+
function homeDir() {
|
|
28
|
+
return os.homedir();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Directory where `dart pub global activate` drops executables.
|
|
33
|
+
* - Honors $PUB_CACHE when the user customized it (works on every OS).
|
|
34
|
+
* - Windows: %LOCALAPPDATA%\Pub\Cache\bin (modern Dart) with %APPDATA% as a
|
|
35
|
+
* legacy fallback. We prefer whichever already exists on disk.
|
|
36
|
+
* - macOS/Linux: ~/.pub-cache/bin
|
|
37
|
+
*/
|
|
38
|
+
function pubCacheBinDir() {
|
|
39
|
+
// Use the platform-specific path flavor explicitly (path.win32 / path.posix)
|
|
40
|
+
// so separators are correct even when this logic is unit-tested from another
|
|
41
|
+
// OS — in production isWindows already matches the running platform.
|
|
42
|
+
const p = isWindows ? path.win32 : path.posix;
|
|
43
|
+
if (process.env.PUB_CACHE) {
|
|
44
|
+
return p.join(process.env.PUB_CACHE, 'bin');
|
|
45
|
+
}
|
|
46
|
+
if (isWindows) {
|
|
47
|
+
const candidates = [
|
|
48
|
+
process.env.LOCALAPPDATA && p.join(process.env.LOCALAPPDATA, 'Pub', 'Cache', 'bin'),
|
|
49
|
+
process.env.APPDATA && p.join(process.env.APPDATA, 'Pub', 'Cache', 'bin'),
|
|
50
|
+
].filter(Boolean);
|
|
51
|
+
for (const candidate of candidates) {
|
|
52
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
53
|
+
}
|
|
54
|
+
return candidates[0] || p.join(homeDir(), 'AppData', 'Local', 'Pub', 'Cache', 'bin');
|
|
55
|
+
}
|
|
56
|
+
return p.join(homeDir(), '.pub-cache', 'bin');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Absolute path to a pub-global executable, quoted and ready to drop into a
|
|
61
|
+
* shell command. On Windows the activated binary is a `.bat` shim.
|
|
62
|
+
*
|
|
63
|
+
* @param {string} name e.g. 'flutterfire'
|
|
64
|
+
* @returns {string} quoted absolute path, or '' if it can't be located
|
|
65
|
+
*/
|
|
66
|
+
function pubCacheBin(name) {
|
|
67
|
+
const dir = pubCacheBinDir();
|
|
68
|
+
const file = isWindows ? path.win32.join(dir, `${name}.bat`) : path.posix.join(dir, name);
|
|
69
|
+
return `"${file}"`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* A copy of process.env with `extraDirs` (plus the pub-cache bin) prepended to
|
|
74
|
+
* PATH, so child processes can find tools installed earlier in this same run.
|
|
75
|
+
* On Windows we set both `PATH` and `Path` because Node reads the original key.
|
|
76
|
+
*
|
|
77
|
+
* @param {string[]} [extraDirs] additional directories to expose
|
|
78
|
+
*/
|
|
79
|
+
function augmentedEnv(extraDirs = []) {
|
|
80
|
+
const dirs = [pubCacheBinDir(), ...extraDirs].filter(Boolean);
|
|
81
|
+
const env = { ...process.env };
|
|
82
|
+
// Windows env keys are case-insensitive; find whichever key holds PATH.
|
|
83
|
+
const pathKey = Object.keys(env).find((k) => k.toLowerCase() === 'path') || 'PATH';
|
|
84
|
+
const current = env[pathKey] || '';
|
|
85
|
+
const existing = current.split(PATH_SEP);
|
|
86
|
+
const additions = dirs.filter((dir) => !existing.includes(dir));
|
|
87
|
+
if (additions.length > 0) {
|
|
88
|
+
const next = current ? `${additions.join(PATH_SEP)}${PATH_SEP}${current}` : additions.join(PATH_SEP);
|
|
89
|
+
env[pathKey] = next;
|
|
90
|
+
if (isWindows) env.Path = next; // belt-and-suspenders for case-sensitive reads
|
|
91
|
+
}
|
|
92
|
+
return env;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
module.exports = {
|
|
96
|
+
isWindows,
|
|
97
|
+
homeDir,
|
|
98
|
+
pubCacheBinDir,
|
|
99
|
+
pubCacheBin,
|
|
100
|
+
augmentedEnv,
|
|
101
|
+
};
|
|
@@ -122,6 +122,14 @@ module.exports = {
|
|
|
122
122
|
'checks.install.failed': 'auto-install failed — run the command manually',
|
|
123
123
|
'checks.waitPrompt.gcloud.install': 'After installing gcloud, press Enter to check again',
|
|
124
124
|
'checks.waitPrompt.gcloud.auth': 'After running gcloud auth login, press Enter to check again',
|
|
125
|
+
'checks.thenRun': 'Then run',
|
|
126
|
+
'checks.recheck': 'After installing {name}, press Enter to check again',
|
|
127
|
+
'checks.install.confirm': 'Install {name} now? ({cmd})',
|
|
128
|
+
'checks.install.running': 'Installing {name}…',
|
|
129
|
+
'checks.install.failedManual': 'Could not install {name} automatically. Do it manually:',
|
|
130
|
+
'checks.install.openDocs': 'Open the install page in the browser?',
|
|
131
|
+
'browser.open.intro': 'Open this link in your browser:',
|
|
132
|
+
'browser.open.confirm': 'Open it in the browser now?',
|
|
125
133
|
'error.hint.notFlutterProject': 'You\'re not inside a Flutter project. Try `kasy new` to create one, or cd into an existing one.',
|
|
126
134
|
'error.hint.flutterMissing': 'Flutter is not installed or not on your PATH. Run `kasy doctor` to diagnose.',
|
|
127
135
|
'error.hint.permission': 'A file or folder is read-only. Check the parent directory permissions or try again from your home folder.',
|
|
@@ -122,6 +122,14 @@ module.exports = {
|
|
|
122
122
|
'checks.install.failed': 'instalación automática falló — ejecuta el comando manualmente',
|
|
123
123
|
'checks.waitPrompt.gcloud.install': 'Tras instalar gcloud, presiona Enter para verificar de nuevo',
|
|
124
124
|
'checks.waitPrompt.gcloud.auth': 'Tras ejecutar gcloud auth login, presiona Enter para verificar de nuevo',
|
|
125
|
+
'checks.thenRun': 'Luego ejecuta',
|
|
126
|
+
'checks.recheck': 'Tras instalar {name}, presiona Enter para verificar de nuevo',
|
|
127
|
+
'checks.install.confirm': '¿Instalar {name} ahora? ({cmd})',
|
|
128
|
+
'checks.install.running': 'Instalando {name}…',
|
|
129
|
+
'checks.install.failedManual': 'No pude instalar {name} automáticamente. Hazlo manualmente:',
|
|
130
|
+
'checks.install.openDocs': '¿Abrir la página de instalación en el navegador?',
|
|
131
|
+
'browser.open.intro': 'Abre este enlace en el navegador:',
|
|
132
|
+
'browser.open.confirm': '¿Abrir en el navegador ahora?',
|
|
125
133
|
'error.hint.notFlutterProject': 'No estás dentro de un proyecto Flutter. Usa `kasy new` para crear uno, o entra en la carpeta de un proyecto existente.',
|
|
126
134
|
'error.hint.flutterMissing': 'Flutter no está instalado o no está en el PATH. Ejecuta `kasy doctor` para diagnosticar.',
|
|
127
135
|
'error.hint.permission': 'Un archivo/carpeta es de solo lectura. Verifica los permisos de la carpeta padre o intenta desde tu carpeta home.',
|
|
@@ -122,6 +122,14 @@ module.exports = {
|
|
|
122
122
|
'checks.install.failed': 'instalação automática falhou — execute o comando manualmente',
|
|
123
123
|
'checks.waitPrompt.gcloud.install': 'Após instalar o gcloud, pressione Enter para verificar novamente',
|
|
124
124
|
'checks.waitPrompt.gcloud.auth': 'Após rodar gcloud auth login, pressione Enter para verificar novamente',
|
|
125
|
+
'checks.thenRun': 'Depois rode',
|
|
126
|
+
'checks.recheck': 'Após instalar {name}, pressione Enter para verificar novamente',
|
|
127
|
+
'checks.install.confirm': 'Instalar {name} agora? ({cmd})',
|
|
128
|
+
'checks.install.running': 'Instalando {name}…',
|
|
129
|
+
'checks.install.failedManual': 'Não consegui instalar {name} automaticamente. Faça manualmente:',
|
|
130
|
+
'checks.install.openDocs': 'Abrir a página de instalação no navegador?',
|
|
131
|
+
'browser.open.intro': 'Abra este link no navegador:',
|
|
132
|
+
'browser.open.confirm': 'Abrir no navegador agora?',
|
|
125
133
|
'error.hint.notFlutterProject': 'Você não está dentro de um projeto Flutter. Use `kasy new` para criar um, ou entre na pasta de um projeto existente.',
|
|
126
134
|
'error.hint.flutterMissing': 'Flutter não está instalado ou não está no PATH. Rode `kasy doctor` para diagnosticar.',
|
|
127
135
|
'error.hint.permission': 'Algum arquivo/pasta está somente leitura. Verifique as permissões da pasta pai ou tente de novo a partir da sua pasta home.',
|
package/package.json
CHANGED
|
@@ -244,10 +244,6 @@ class _KasySidebarProState extends State<KasySidebarPro> {
|
|
|
244
244
|
final c = _colors;
|
|
245
245
|
|
|
246
246
|
final bool anchoredLeft = widget.side == KasySidebarProSide.left;
|
|
247
|
-
const Radius r = Radius.circular(12);
|
|
248
|
-
final BorderRadius borderRadius = anchoredLeft
|
|
249
|
-
? const BorderRadius.only(topRight: r, bottomRight: r)
|
|
250
|
-
: const BorderRadius.only(topLeft: r, bottomLeft: r);
|
|
251
247
|
final Border border = anchoredLeft
|
|
252
248
|
? Border(right: BorderSide(color: c.border))
|
|
253
249
|
: Border(left: BorderSide(color: c.border));
|
|
@@ -282,16 +278,12 @@ class _KasySidebarProState extends State<KasySidebarPro> {
|
|
|
282
278
|
right: anchoredLeft ? halfBtn : 0,
|
|
283
279
|
top: 0,
|
|
284
280
|
bottom: 0,
|
|
285
|
-
child:
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
color: c.bg,
|
|
290
|
-
border: border,
|
|
291
|
-
borderRadius: borderRadius,
|
|
292
|
-
),
|
|
293
|
-
child: _buildScrollableContent(context, c),
|
|
281
|
+
child: DecoratedBox(
|
|
282
|
+
decoration: BoxDecoration(
|
|
283
|
+
color: c.bg,
|
|
284
|
+
border: border,
|
|
294
285
|
),
|
|
286
|
+
child: _buildScrollableContent(context, c),
|
|
295
287
|
),
|
|
296
288
|
),
|
|
297
289
|
// Collapse button — centered on the sidebar edge, fully within
|
|
@@ -44,6 +44,7 @@ class KasyHover extends ConsumerStatefulWidget {
|
|
|
44
44
|
this.margin,
|
|
45
45
|
this.semanticLabel,
|
|
46
46
|
this.hapticEnabled = true,
|
|
47
|
+
this.hoverEnabled = true,
|
|
47
48
|
this.hoverColor,
|
|
48
49
|
this.pressColor,
|
|
49
50
|
});
|
|
@@ -67,6 +68,13 @@ class KasyHover extends ConsumerStatefulWidget {
|
|
|
67
68
|
/// Haptic on tap (automatically ignored on web).
|
|
68
69
|
final bool hapticEnabled;
|
|
69
70
|
|
|
71
|
+
/// Whether the pointer-hover overlay is shown on pointer-capable devices.
|
|
72
|
+
///
|
|
73
|
+
/// When false, the resting/press feedback and click cursor are kept, but no
|
|
74
|
+
/// background fill appears while hovering. Use on plain list rows (e.g.
|
|
75
|
+
/// settings) where a hover highlight is visually unwanted.
|
|
76
|
+
final bool hoverEnabled;
|
|
77
|
+
|
|
70
78
|
/// Exact background colour used while the pointer hovers over the widget.
|
|
71
79
|
///
|
|
72
80
|
/// When non-null, this solid colour replaces the default semi-transparent
|
|
@@ -110,7 +118,7 @@ class _KasyHoverState extends ConsumerState<KasyHover> {
|
|
|
110
118
|
final bool isDark = Theme.of(context).brightness == Brightness.dark;
|
|
111
119
|
final Color base = widget.pressColor ?? context.colors.onSurface;
|
|
112
120
|
if (_pressed) return base.withValues(alpha: isDark ? 0.04 : 0.10);
|
|
113
|
-
if (_hovered) {
|
|
121
|
+
if (_hovered && widget.hoverEnabled) {
|
|
114
122
|
// Solid hover colour — used for nav items where hover must exactly
|
|
115
123
|
// match the selected background.
|
|
116
124
|
if (widget.hoverColor != null) return widget.hoverColor!;
|
|
@@ -155,6 +155,7 @@ class SettingsTile extends StatelessWidget {
|
|
|
155
155
|
Widget build(BuildContext context) {
|
|
156
156
|
return KasyHover(
|
|
157
157
|
onTap: onTap,
|
|
158
|
+
hoverEnabled: false,
|
|
158
159
|
semanticLabel: title,
|
|
159
160
|
padding: const EdgeInsets.symmetric(vertical: KasySpacing.sm),
|
|
160
161
|
child: Row(
|