kasy-cli 1.20.0 → 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/README.md +11 -3
- package/lib/commands/docs.js +0 -10
- package/lib/commands/ios.js +3 -2
- package/lib/commands/new.js +98 -58
- package/lib/commands/run.js +7 -0
- package/lib/scaffold/CHANGELOG.json +14 -0
- package/lib/scaffold/backends/api/patch/lib/core/data/api/meta_ads_api.dart +1 -1
- package/lib/scaffold/backends/api/patch/lib/core/data/api/storage_api.dart +1 -1
- package/lib/scaffold/backends/api/patch/lib/core/data/entities/user_entity.dart +1 -1
- package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api.dart +4 -5
- package/lib/scaffold/backends/api/patch/lib/features/feedbacks/api/feature_request_api.dart +1 -1
- package/lib/scaffold/backends/api/patch/lib/features/feedbacks/api/feature_vote_api.dart +1 -1
- package/lib/scaffold/backends/api/patch/lib/features/llm_chat/api/llm_chat_api.dart +1 -1
- package/lib/scaffold/backends/api/patch/lib/features/llm_chat/providers/llm_chat_notifier.dart +317 -0
- package/lib/scaffold/backends/api/patch/lib/features/notifications/api/device_api.dart +40 -1
- package/lib/scaffold/backends/api/patch/lib/features/notifications/api/entities/notifications_entity.dart +2 -0
- package/lib/scaffold/backends/api/patch/lib/features/onboarding/api/entities/user_info_entity.dart +1 -1
- package/lib/scaffold/backends/api/patch/lib/features/subscription/api/entities/subscription_entity.dart +1 -1
- package/lib/scaffold/backends/api/patch/lib/features/subscription/shared/maybeshow_premium.dart +0 -2
- package/lib/scaffold/backends/api/pubspec.yaml.tpl +2 -0
- package/lib/scaffold/backends/firebase/enable-auth-via-cli.js +11 -8
- package/lib/scaffold/backends/firebase/setup-from-scratch.js +10 -8
- package/lib/scaffold/backends/supabase/deploy.js +56 -3
- package/lib/scaffold/backends/supabase/patch/lib/core/data/api/storage_api.dart +5 -11
- package/lib/scaffold/backends/supabase/patch/lib/core/data/entities/user_entity.dart +2 -2
- package/lib/scaffold/backends/supabase/patch/lib/features/notifications/api/device_api.dart +31 -1
- package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/api/entities/user_info_entity.dart +1 -1
- package/lib/scaffold/backends/supabase/patch/lib/features/subscription/api/entities/subscription_entity.dart +1 -1
- package/lib/scaffold/backends/supabase/pubspec.yaml.tpl +2 -0
- package/lib/scaffold/catalog.js +2 -2
- package/lib/scaffold/generate.js +19 -3
- package/lib/scaffold/shared/generator-utils.js +265 -55
- package/lib/scaffold/shared/post-build.js +22 -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 +13 -1
- package/lib/utils/i18n/messages-es.js +13 -1
- package/lib/utils/i18n/messages-pt.js +13 -1
- package/package.json +1 -1
- package/templates/firebase/lib/components/kasy_sidebar_pro.dart +8 -14
- package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +38 -128
- package/templates/firebase/lib/core/bottom_menu/bottom_router.dart +6 -125
- package/templates/firebase/lib/core/states/user_state_notifier.dart +8 -10
- package/templates/firebase/lib/core/widgets/kasy_hover.dart +9 -1
- package/templates/firebase/lib/features/home/home_components_page.dart +8 -14
- package/templates/firebase/lib/features/home/home_page.dart +7 -8
- package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +1 -0
- package/templates/firebase/lib/router.dart +60 -0
- package/templates/firebase/test/core/bottom_menu/detail_route_menu_test.dart +57 -0
- package/lib/scaffold/backends/api/patch/lib/core/rating/widgets/review_popup.dart +0 -211
- package/lib/scaffold/backends/api/patch/lib/features/notifications/providers/models/notification.dart +0 -185
- package/lib/scaffold/backends/api/patch/lib/features/onboarding/ui/components/onboarding_notifications_setup.dart +0 -73
- package/lib/scaffold/backends/api/patch/lib/main.dart +0 -275
- package/lib/scaffold/backends/api/patch/lib/router.dart +0 -133
- package/lib/scaffold/backends/supabase/patch/lib/core/rating/widgets/review_popup.dart +0 -211
- package/lib/scaffold/backends/supabase/patch/lib/features/feedbacks/ui/component/add_feature_form.dart +0 -199
- package/lib/scaffold/backends/supabase/patch/lib/features/notifications/providers/models/notification.dart +0 -174
- package/lib/scaffold/backends/supabase/patch/lib/features/onboarding/ui/components/onboarding_notifications_setup.dart +0 -73
- package/lib/scaffold/backends/supabase/patch/lib/main.dart +0 -307
- package/lib/scaffold/backends/supabase/patch/lib/router.dart +0 -133
|
@@ -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
|
}
|
|
@@ -864,6 +869,16 @@ async function getGoogleClientSecretViaGcloud(firebaseProjectId) {
|
|
|
864
869
|
}
|
|
865
870
|
}
|
|
866
871
|
|
|
872
|
+
// Deploys only Firestore security rules so the generated app works immediately
|
|
873
|
+
// without needing `kasy deploy`. Fast (<30s) and billing-free.
|
|
874
|
+
async function deployFirestoreRules(projectDir, firebaseProjectId) {
|
|
875
|
+
return run(
|
|
876
|
+
`firebase deploy --only firestore:rules --project ${firebaseProjectId}`,
|
|
877
|
+
projectDir,
|
|
878
|
+
120_000, // 2 min
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
|
|
867
882
|
module.exports = {
|
|
868
883
|
pubGet,
|
|
869
884
|
slangGenerate,
|
|
@@ -880,6 +895,7 @@ module.exports = {
|
|
|
880
895
|
validateFacebookAndroidStrings,
|
|
881
896
|
validateRevenueCat,
|
|
882
897
|
patchFirebaseServiceWorker,
|
|
898
|
+
deployFirestoreRules,
|
|
883
899
|
readSupabaseGoogleCredentials,
|
|
884
900
|
getGoogleClientSecretViaGcloud,
|
|
885
901
|
// Exported for focused unit tests; not part of the public CLI surface.
|
|
@@ -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.',
|
|
@@ -524,7 +532,7 @@ module.exports = {
|
|
|
524
532
|
'new.firebase.module.sentry': '🚨 Crash Reports (Sentry)',
|
|
525
533
|
'new.firebase.module.analytics': '📊 Analytics (Mixpanel)',
|
|
526
534
|
'new.firebase.module.facebook': '👤 Facebook (Login + Ads)',
|
|
527
|
-
'new.firebase.module.web': '🌐 Web Support (PWA
|
|
535
|
+
'new.firebase.module.web': '🌐 Web Support (PWA)',
|
|
528
536
|
'new.firebase.module.widget': '📱 Home Widget (iOS/Android)',
|
|
529
537
|
'new.firebase.module.llm_chat': '🤖 AI Chat (OpenAI/Gemini)',
|
|
530
538
|
'new.firebase.module.local_notifications': '🔔 Local Reminders (no server)',
|
|
@@ -692,6 +700,10 @@ module.exports = {
|
|
|
692
700
|
'new.google.refreshConfigs': 'Updating google-services.json and GoogleService-Info.plist with Google Client IDs…',
|
|
693
701
|
'new.google.manualHint': 'Google Sign-In: enable manually in the Console (Google provider):',
|
|
694
702
|
'new.google.manualHint.noEmail': 'Google Sign-In: could not detect a support email (gcloud has no account). Enable manually in the Console:',
|
|
703
|
+
'new.google.supabaseManual': 'Google Sign-In: client created, but the secret was not available yet. Enable it later in the Supabase dashboard (Authentication > Providers > Google).',
|
|
704
|
+
'new.fcm.ok': 'generated automatically',
|
|
705
|
+
'new.fcm.failSupabase': 'not generated (GCP permission still propagating); set FIREBASE_SERVICE_ACCOUNT_JSON in your Supabase secrets',
|
|
706
|
+
'new.fcm.failApi': 'not generated (GCP permission still propagating); run the command again in a few minutes',
|
|
695
707
|
'new.sha1.registering': 'Registering SHA-1 for Google Sign-In (Android)…',
|
|
696
708
|
'new.sha1.failed': 'SHA-1 not added automatically: {error}',
|
|
697
709
|
'new.sha1.manual': 'Add it manually so Google Sign-In works on Android:',
|