kasy-cli 1.27.0 → 1.29.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/new.js +79 -30
- package/lib/scaffold/backends/firebase/deploy.js +5 -0
- package/lib/scaffold/backends/firebase/enable-auth-via-cli.js +8 -2
- package/lib/scaffold/backends/firebase/setup-from-scratch.js +15 -1
- package/lib/utils/checks.js +44 -24
- package/lib/utils/env-tools.js +41 -2
- package/lib/utils/flutter-install.js +59 -1
- package/lib/utils/i18n/messages-en.js +2 -1
- package/lib/utils/i18n/messages-es.js +2 -1
- package/lib/utils/i18n/messages-pt.js +2 -1
- package/package.json +1 -1
- package/templates/firebase/lib/core/web_device_preview/web_device_preview.dart +9 -1
package/lib/commands/new.js
CHANGED
|
@@ -154,6 +154,11 @@ async function confirmIdentities(backend, gcloudAccount, tr, cancel) {
|
|
|
154
154
|
const doLogin = await ui.confirm({ message: tr('new.accounts.loginPrompt', { name }), initialValue: true, onCancel: cancel });
|
|
155
155
|
if (!doLogin) return;
|
|
156
156
|
ui.log.message(tr('new.accounts.loggingIn', { name }));
|
|
157
|
+
// Firebase's first run interrupts the login with two unrelated yes/no
|
|
158
|
+
// prompts (Gemini in Firebase, anonymous usage data). For a non-technical
|
|
159
|
+
// user mid-setup that's just noise that can derail them. Opt out silently so
|
|
160
|
+
// `firebase login` goes straight to the browser.
|
|
161
|
+
if (name === 'Firebase') silenceFirebaseFirstRunPrompts();
|
|
157
162
|
const { spawnSync } = require('node:child_process');
|
|
158
163
|
spawnSync(cmd, { stdio: 'inherit', shell: true });
|
|
159
164
|
await revalidate();
|
|
@@ -195,6 +200,47 @@ async function confirmIdentities(backend, gcloudAccount, tr, cancel) {
|
|
|
195
200
|
}
|
|
196
201
|
}
|
|
197
202
|
|
|
203
|
+
/**
|
|
204
|
+
* Opt out of Firebase CLI's first-run questions before we shell into
|
|
205
|
+
* `firebase login`. On a fresh machine the CLI otherwise stops to ask, one at a
|
|
206
|
+
* time, whether to enable Gemini in Firebase and whether to share anonymous
|
|
207
|
+
* usage data — both irrelevant to creating a project and confusing for a
|
|
208
|
+
* first-time user who just wants to log in. firebase-tools reads these from a
|
|
209
|
+
* configstore JSON (~/.config/configstore/firebase-tools.json on every OS); we
|
|
210
|
+
* only write keys that aren't already set, so a user who answered them before
|
|
211
|
+
* keeps their choice. Best-effort: any failure is ignored, the prompts just
|
|
212
|
+
* reappear. Never throws.
|
|
213
|
+
*/
|
|
214
|
+
function silenceFirebaseFirstRunPrompts() {
|
|
215
|
+
try {
|
|
216
|
+
const fs = require('node:fs');
|
|
217
|
+
const os = require('node:os');
|
|
218
|
+
const path = require('node:path');
|
|
219
|
+
// firebase-tools honors XDG_CONFIG_HOME, falling back to ~/.config.
|
|
220
|
+
const cfgHome =
|
|
221
|
+
process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
|
|
222
|
+
const dir = path.join(cfgHome, 'configstore');
|
|
223
|
+
const file = path.join(dir, 'firebase-tools.json');
|
|
224
|
+
let data = {};
|
|
225
|
+
if (fs.existsSync(file)) {
|
|
226
|
+
try {
|
|
227
|
+
data = JSON.parse(fs.readFileSync(file, 'utf8')) || {};
|
|
228
|
+
} catch {
|
|
229
|
+
data = {};
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
let changed = false;
|
|
233
|
+
if (!('usage' in data)) { data.usage = false; changed = true; }
|
|
234
|
+
if (!('gemini' in data)) { data.gemini = false; changed = true; }
|
|
235
|
+
if (changed) {
|
|
236
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
237
|
+
fs.writeFileSync(file, JSON.stringify(data, null, 2));
|
|
238
|
+
}
|
|
239
|
+
} catch {
|
|
240
|
+
// Best-effort: if we can't write the config, the prompts just show up.
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
198
244
|
/**
|
|
199
245
|
* Try to get gcloud ready (installed + logged in) without making the user leave
|
|
200
246
|
* the flow. Mirrors the machine-prep we do for Node/Flutter/Firebase: offer to
|
|
@@ -211,37 +257,34 @@ async function ensureGcloudReady(tr) {
|
|
|
211
257
|
let check = await checkGcloudAuth();
|
|
212
258
|
if (check.ok) return check;
|
|
213
259
|
|
|
214
|
-
// 1) Not installed →
|
|
260
|
+
// 1) Not installed → install it automatically. gcloud is mandatory to create a
|
|
261
|
+
// Firebase project from scratch, so a yes/no here just adds a dead end the
|
|
262
|
+
// user can fall into; we install it the same way we do Node/Flutter/Firebase
|
|
263
|
+
// (auto, with visible progress) and only tell them what's happening.
|
|
215
264
|
if (check.missing === 'gcloud') {
|
|
216
265
|
const guide = getGcloudInstallInstructions();
|
|
217
266
|
if (guide.install) {
|
|
218
|
-
ui.log.
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
267
|
+
ui.log.info(tr('new.firebase.create.gcloudMissing'));
|
|
268
|
+
const spinner = ui.timedSpinner();
|
|
269
|
+
spinner.start(tr('new.firebase.create.gcloudInstalling'));
|
|
270
|
+
// Run async (NOT spawnSync): a synchronous child blocks the event loop,
|
|
271
|
+
// which freezes the spinner — the user stares at a dead screen for
|
|
272
|
+
// minutes. exec keeps the clock ticking so it's clear it's working.
|
|
273
|
+
await new Promise((resolve) => {
|
|
274
|
+
exec(
|
|
275
|
+
guide.install,
|
|
276
|
+
{ env: augmentedEnv(), timeout: 600_000, windowsHide: true, maxBuffer: 50 * 1024 * 1024 },
|
|
277
|
+
() => resolve()
|
|
278
|
+
);
|
|
222
279
|
});
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
guide.install,
|
|
232
|
-
{ env: augmentedEnv(), timeout: 600_000, windowsHide: true, maxBuffer: 50 * 1024 * 1024 },
|
|
233
|
-
() => resolve()
|
|
234
|
-
);
|
|
235
|
-
});
|
|
236
|
-
spinner.stop(tr('new.firebase.create.gcloudInstalling'));
|
|
237
|
-
// checkGcloudAuth and the later billing/project calls use the plain
|
|
238
|
-
// process PATH, which doesn't see a tool winget just put on the machine
|
|
239
|
-
// PATH. Inject the known gcloud dir so the rest of the flow works.
|
|
240
|
-
const env = augmentedEnv();
|
|
241
|
-
if (env.PATH) process.env.PATH = env.PATH;
|
|
242
|
-
if (process.platform === 'win32' && env.Path) process.env.Path = env.Path;
|
|
243
|
-
check = await checkGcloudAuth();
|
|
244
|
-
}
|
|
280
|
+
spinner.stop(tr('new.firebase.create.gcloudInstalling'));
|
|
281
|
+
// checkGcloudAuth and the later billing/project calls use the plain
|
|
282
|
+
// process PATH, which doesn't see a tool winget just put on the machine
|
|
283
|
+
// PATH. Inject the known gcloud dir so the rest of the flow works.
|
|
284
|
+
const env = augmentedEnv();
|
|
285
|
+
if (env.PATH) process.env.PATH = env.PATH;
|
|
286
|
+
if (process.platform === 'win32' && env.Path) process.env.Path = env.Path;
|
|
287
|
+
check = await checkGcloudAuth();
|
|
245
288
|
}
|
|
246
289
|
}
|
|
247
290
|
|
|
@@ -563,20 +606,26 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
|
|
|
563
606
|
const tr = createTranslator(language);
|
|
564
607
|
const cancel = () => onCancel(tr);
|
|
565
608
|
|
|
609
|
+
// Show the brand right away so the first thing the user sees is the logo, not
|
|
610
|
+
// a couple of silent seconds while we probe the toolchain below.
|
|
611
|
+
printBanner(tr);
|
|
612
|
+
|
|
566
613
|
// ── 1b. Version check — warn if outdated, let user decide ───────────────
|
|
567
614
|
if (!yes) {
|
|
568
615
|
await warnIfOutdatedBeforeNew(tr);
|
|
569
616
|
}
|
|
570
617
|
|
|
571
618
|
// ── 2. Environment checks (non-blocking — only warnings) ────────────────
|
|
619
|
+
// quiet:true keeps it to a single short "Checking environment…" line (no
|
|
620
|
+
// per-tool detail). It only gets noisy if a tool is actually missing and has
|
|
621
|
+
// to be installed — exactly when the user wants to know.
|
|
572
622
|
const envChecks = [...getBaseChecks(), ...getPlatformChecks()];
|
|
573
|
-
await runChecks(envChecks, tr('new.checks.environment'), {
|
|
623
|
+
await runChecks(envChecks, tr('new.checks.environment.checking'), {
|
|
574
624
|
t: tr,
|
|
575
625
|
quiet: true,
|
|
626
|
+
doneLabel: tr('new.checks.environment.done'),
|
|
576
627
|
});
|
|
577
628
|
|
|
578
|
-
printBanner(tr);
|
|
579
|
-
|
|
580
629
|
ui.intro(kleur.bold(tr('new.subtitle2')));
|
|
581
630
|
|
|
582
631
|
// Whether an explicit target directory was provided by the user
|
|
@@ -14,6 +14,7 @@ const { promisify } = require('node:util');
|
|
|
14
14
|
const path = require('node:path');
|
|
15
15
|
const os = require('node:os');
|
|
16
16
|
const fs = require('fs-extra');
|
|
17
|
+
const { augmentedEnv } = require('../../../utils/env-tools');
|
|
17
18
|
|
|
18
19
|
const execAsync = promisify(exec);
|
|
19
20
|
|
|
@@ -160,6 +161,10 @@ async function run(cmd, cwd) {
|
|
|
160
161
|
const { stdout, stderr } = await execAsync(cmd, {
|
|
161
162
|
cwd,
|
|
162
163
|
maxBuffer: 50 * 1024 * 1024,
|
|
164
|
+
// Same reason as setup-from-scratch: find tools (gcloud, firebase, npm)
|
|
165
|
+
// that may have been installed earlier in this run but aren't on the
|
|
166
|
+
// default Windows process PATH yet.
|
|
167
|
+
env: augmentedEnv(),
|
|
163
168
|
});
|
|
164
169
|
return { ok: true, stdout, stderr };
|
|
165
170
|
} catch (err) {
|
|
@@ -15,12 +15,18 @@ const path = require('node:path');
|
|
|
15
15
|
const { promisify } = require('node:util');
|
|
16
16
|
const execAsync = promisify(require('node:child_process').exec);
|
|
17
17
|
const fs = require('fs-extra');
|
|
18
|
+
const { augmentedEnv } = require('../../../utils/env-tools');
|
|
18
19
|
|
|
19
20
|
const DEPLOY_TIMEOUT_MS = 5 * 60 * 1000; // 5 min — auth deploys are fast but allow slack
|
|
20
21
|
|
|
21
22
|
async function getGcloudAccountEmail() {
|
|
23
|
+
// No shell-specific redirect here: `2>/dev/null` is a Unix-ism that cmd.exe
|
|
24
|
+
// does NOT understand, so on Windows it made the whole command fail and we
|
|
25
|
+
// wrongly reported "no gcloud account" even when the user was logged in.
|
|
26
|
+
// gcloud prints the account to stdout; any "(unset)" note goes to stderr,
|
|
27
|
+
// which exec already keeps separate, so we don't need to silence it.
|
|
22
28
|
try {
|
|
23
|
-
const { stdout } = await execAsync('gcloud config get-value account
|
|
29
|
+
const { stdout } = await execAsync('gcloud config get-value account', { env: augmentedEnv() });
|
|
24
30
|
const email = (stdout || '').trim();
|
|
25
31
|
return email && email !== '(unset)' ? email : null;
|
|
26
32
|
} catch {
|
|
@@ -98,7 +104,7 @@ async function enableAuthViaFirebaseCli({ projectDir, projectId, appName, suppor
|
|
|
98
104
|
// 3. Deploy. --non-interactive prevents the CLI from prompting on edge cases.
|
|
99
105
|
const cmd = `firebase deploy --only auth --project ${projectId} --non-interactive`;
|
|
100
106
|
try {
|
|
101
|
-
await execAsync(cmd, { cwd: projectDir, timeout: DEPLOY_TIMEOUT_MS, maxBuffer: 10 * 1024 * 1024 });
|
|
107
|
+
await execAsync(cmd, { cwd: projectDir, timeout: DEPLOY_TIMEOUT_MS, maxBuffer: 10 * 1024 * 1024, env: augmentedEnv() });
|
|
102
108
|
return { ok: true, supportEmail: email };
|
|
103
109
|
} catch (err) {
|
|
104
110
|
const stderr = (err.stderr || '').toString();
|
|
@@ -19,7 +19,8 @@ const path = require('node:path');
|
|
|
19
19
|
const fs = require('fs-extra');
|
|
20
20
|
const os = require('node:os');
|
|
21
21
|
const { getInstallGuide } = require('../../../utils/checks');
|
|
22
|
-
const { keytoolBin } = require('../../../utils/env-tools');
|
|
22
|
+
const { keytoolBin, augmentedEnv } = require('../../../utils/env-tools');
|
|
23
|
+
const { ensureJdkWindows } = require('../../../utils/flutter-install');
|
|
23
24
|
|
|
24
25
|
const execAsync = promisify(exec);
|
|
25
26
|
|
|
@@ -66,6 +67,11 @@ async function run(cmd, cwd = process.cwd()) {
|
|
|
66
67
|
cwd,
|
|
67
68
|
maxBuffer: 10 * 1024 * 1024,
|
|
68
69
|
encoding: 'utf8',
|
|
70
|
+
// augmentedEnv() injects the install dirs of tools we may have just put on
|
|
71
|
+
// the machine this run (gcloud, Flutter/Dart, Git, the JDK for keytool).
|
|
72
|
+
// On Windows those aren't on the default process PATH yet, so without this
|
|
73
|
+
// every gcloud/keytool call here would fail with "not recognized".
|
|
74
|
+
env: augmentedEnv(),
|
|
69
75
|
});
|
|
70
76
|
return { ok: true, stdout: (stdout || '').trim(), stderr: (stderr || '').trim() };
|
|
71
77
|
} catch (err) {
|
|
@@ -533,6 +539,14 @@ async function createWebApp(projectId, appName) {
|
|
|
533
539
|
* Ensure debug.keystore exists. Creates it if missing (standard Android debug key).
|
|
534
540
|
*/
|
|
535
541
|
async function ensureDebugKeystore() {
|
|
542
|
+
// On Windows a fresh machine has no JDK, so keytool is missing. If we can't
|
|
543
|
+
// resolve a real keytool path (keytoolBin falls back to the bare name), try
|
|
544
|
+
// installing Microsoft OpenJDK once so the steps below actually find it. This
|
|
545
|
+
// covers the case where Flutter was already present (so its installer, which
|
|
546
|
+
// also installs the JDK, never ran this session). Best-effort.
|
|
547
|
+
if (process.platform === 'win32' && keytoolBin() === 'keytool') {
|
|
548
|
+
await ensureJdkWindows();
|
|
549
|
+
}
|
|
536
550
|
const androidDir = path.dirname(DEBUG_KEYSTORE);
|
|
537
551
|
if (!(await fs.pathExists(DEBUG_KEYSTORE))) {
|
|
538
552
|
await fs.ensureDir(androidDir);
|
package/lib/utils/checks.js
CHANGED
|
@@ -275,34 +275,33 @@ async function recoverCheckInteractively(check, t) {
|
|
|
275
275
|
// The step list already flagged this tool as missing; go straight to fixing it.
|
|
276
276
|
const guide = typeof check.installGuide === 'function' ? check.installGuide() : null;
|
|
277
277
|
|
|
278
|
-
// 1)
|
|
278
|
+
// 1) Auto-install heavy tools. Most use a package-manager command
|
|
279
279
|
// (brew/winget); Flutter on Windows uses a custom installer (check.installFn)
|
|
280
|
-
// because winget ships no stable Flutter package.
|
|
280
|
+
// because winget ships no stable Flutter package. We don't ask a yes/no here:
|
|
281
|
+
// for a Flutter project these tools are required to go any further, so a
|
|
282
|
+
// confirm just adds a dead end the user can fall into. We install
|
|
283
|
+
// automatically and show what's happening (incl. the size note for big
|
|
284
|
+
// downloads), matching how the Firebase/FlutterFire CLIs already behave.
|
|
281
285
|
if (check.confirmInstall && (check.installFn || (guide && guide.cmd))) {
|
|
282
286
|
const cmdLabel = check.installFn
|
|
283
287
|
? (check.installFnDescKey ? t(check.installFnDescKey) : check.name)
|
|
284
288
|
: guide.cmd;
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
// agreement not accepted, package not found), which beats a generic failure.
|
|
302
|
-
const reason = (installed.stderr || installed.error || '')
|
|
303
|
-
.split('\n').map((l) => l.trim()).filter(Boolean).pop();
|
|
304
|
-
if (reason) ui.log.message(kleur.dim(reason.slice(0, 200)));
|
|
305
|
-
}
|
|
289
|
+
// Tell the user we're installing it (and why it can take a while / is big).
|
|
290
|
+
ui.log.info(t('checks.install.autoInstalling', { name: check.name, cmd: cmdLabel }));
|
|
291
|
+
if (check.installFn && check.installFnNoteKey) ui.log.info(t(check.installFnNoteKey));
|
|
292
|
+
const spinner = ui.timedSpinner();
|
|
293
|
+
spinner.start(t('checks.install.running', { name: check.name }));
|
|
294
|
+
const installed = check.installFn
|
|
295
|
+
? await check.installFn({})
|
|
296
|
+
: await execTool(guide.cmd, INSTALL_TIMEOUT);
|
|
297
|
+
spinner.stop(t('checks.install.running', { name: check.name }));
|
|
298
|
+
if (installed.ok && (await revalidate(check, t))) return true;
|
|
299
|
+
ui.log.warn(t('checks.install.failedManual', { name: check.name }));
|
|
300
|
+
// Surface the installer's own last line — it usually says WHY (needs admin,
|
|
301
|
+
// agreement not accepted, package not found), which beats a generic failure.
|
|
302
|
+
const reason = (installed.stderr || installed.error || '')
|
|
303
|
+
.split('\n').map((l) => l.trim()).filter(Boolean).pop();
|
|
304
|
+
if (reason) ui.log.message(kleur.dim(reason.slice(0, 200)));
|
|
306
305
|
}
|
|
307
306
|
|
|
308
307
|
// 2) Manual guidance — the exact command for this OS + an Enter-to-open link.
|
|
@@ -379,6 +378,15 @@ async function runChecks(checks, title, options = {}) {
|
|
|
379
378
|
// (doctor) shows every step. Either way, failures are handled below.
|
|
380
379
|
const quiet = options.quiet === true;
|
|
381
380
|
const stepper = quiet ? null : ui.makeTimedStepper();
|
|
381
|
+
// In quiet mode we'd otherwise probe the toolchain in total silence, leaving
|
|
382
|
+
// the user staring at a couple of dead seconds. Show ONE short spinner with
|
|
383
|
+
// the phase title (no per-tool detail). It hands off to an install spinner if
|
|
384
|
+
// a tool is missing, or clears with `doneLabel` once everything passes.
|
|
385
|
+
let phaseSpinner = null;
|
|
386
|
+
if (quiet && title && process.stdout.isTTY) {
|
|
387
|
+
phaseSpinner = ui.spinner();
|
|
388
|
+
phaseSpinner.start(title);
|
|
389
|
+
}
|
|
382
390
|
const results = [];
|
|
383
391
|
|
|
384
392
|
for (const check of checks) {
|
|
@@ -391,6 +399,8 @@ async function runChecks(checks, title, options = {}) {
|
|
|
391
399
|
onInstalling: (c) => {
|
|
392
400
|
const msg = c.tryInstallMessageKey ? t(c.tryInstallMessageKey) : t('setup.installingNamed', { name: c.name });
|
|
393
401
|
if (stepper) { stepper.update(msg); return; }
|
|
402
|
+
// Hand the line off from the phase spinner to this install's own spinner.
|
|
403
|
+
if (phaseSpinner) { phaseSpinner.stop(title); phaseSpinner = null; }
|
|
394
404
|
installSpinner = ui.timedSpinner();
|
|
395
405
|
installSpinner.start(msg);
|
|
396
406
|
},
|
|
@@ -411,7 +421,17 @@ async function runChecks(checks, title, options = {}) {
|
|
|
411
421
|
if (result.ok) installSpinner.stop(result.version ? `${result.name} — ${result.version}` : result.name);
|
|
412
422
|
else installSpinner.error(t('checks.missing', { name: result.name }));
|
|
413
423
|
}
|
|
414
|
-
// quiet + passed without installing →
|
|
424
|
+
// quiet + passed without installing → the phase spinner stays running.
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Close the phase line. If everything passed, show the friendly done label;
|
|
428
|
+
// if something failed, stop neutrally and let the recovery/failure output
|
|
429
|
+
// below explain it. Stopping here (before the prompts) avoids a spinner
|
|
430
|
+
// animating underneath an interactive question.
|
|
431
|
+
if (phaseSpinner) {
|
|
432
|
+
const allOk = results.every((r) => r.ok);
|
|
433
|
+
phaseSpinner.stop(allOk ? (options.doneLabel || title) : title);
|
|
434
|
+
phaseSpinner = null;
|
|
415
435
|
}
|
|
416
436
|
|
|
417
437
|
const failures = results.filter((r) => !r.ok);
|
package/lib/utils/env-tools.js
CHANGED
|
@@ -69,16 +69,52 @@ function pubCacheBin(name) {
|
|
|
69
69
|
return `"${file}"`;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Find the `bin` dir of a JDK installed on Windows under a versioned folder
|
|
74
|
+
* (e.g. `C:\Program Files\Microsoft\jdk-17.0.13\bin`). winget's Microsoft
|
|
75
|
+
* OpenJDK / Eclipse Adoptium packages install there but the exact version is in
|
|
76
|
+
* the path, so we can't hardcode it — we scan the known vendor roots and pick a
|
|
77
|
+
* folder that actually contains keytool. Returns null on non-Windows or if none
|
|
78
|
+
* is found.
|
|
79
|
+
*/
|
|
80
|
+
function findWindowsJdkBin() {
|
|
81
|
+
if (!isWindows) return null;
|
|
82
|
+
const pf = process.env.ProgramFiles;
|
|
83
|
+
if (!pf) return null;
|
|
84
|
+
const roots = [
|
|
85
|
+
path.win32.join(pf, 'Microsoft'), // Microsoft.OpenJDK.*
|
|
86
|
+
path.win32.join(pf, 'Eclipse Adoptium'), // EclipseAdoptium.Temurin.*
|
|
87
|
+
path.win32.join(pf, 'Java'), // Oracle / other
|
|
88
|
+
];
|
|
89
|
+
for (const root of roots) {
|
|
90
|
+
try {
|
|
91
|
+
if (!fs.existsSync(root)) continue;
|
|
92
|
+
// Highest version first (lexical desc is close enough for jdk-NN.x names).
|
|
93
|
+
const dirs = fs.readdirSync(root).filter((d) => /jdk|jre/i.test(d)).sort().reverse();
|
|
94
|
+
for (const d of dirs) {
|
|
95
|
+
const bin = path.win32.join(root, d, 'bin');
|
|
96
|
+
if (fs.existsSync(path.win32.join(bin, 'keytool.exe'))) return bin;
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
// Unreadable root — skip it.
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
72
105
|
/**
|
|
73
106
|
* Quoted path to `keytool` (used for the Android debug SHA-1). It ships with
|
|
74
|
-
* the JDK and is almost never on PATH on Windows, so we look in JAVA_HOME
|
|
75
|
-
* Android Studio's bundled JBR before falling back to
|
|
107
|
+
* the JDK and is almost never on PATH on Windows, so we look in JAVA_HOME, a
|
|
108
|
+
* winget-installed JDK, and Android Studio's bundled JBR before falling back to
|
|
109
|
+
* bare `keytool`.
|
|
76
110
|
*/
|
|
77
111
|
function keytoolBin() {
|
|
78
112
|
const exe = isWindows ? 'keytool.exe' : 'keytool';
|
|
79
113
|
const candidates = [];
|
|
80
114
|
if (process.env.JAVA_HOME) candidates.push(path.join(process.env.JAVA_HOME, 'bin', exe));
|
|
81
115
|
if (isWindows) {
|
|
116
|
+
const jdkBin = findWindowsJdkBin();
|
|
117
|
+
if (jdkBin) candidates.push(path.win32.join(jdkBin, exe));
|
|
82
118
|
const pf = process.env.ProgramFiles;
|
|
83
119
|
const local = process.env.LOCALAPPDATA;
|
|
84
120
|
if (pf) candidates.push(path.win32.join(pf, 'Android', 'Android Studio', 'jbr', 'bin', exe));
|
|
@@ -112,6 +148,9 @@ function extraToolDirs() {
|
|
|
112
148
|
// Git (a Flutter prerequisite) installed via winget Git.Git — default locations.
|
|
113
149
|
process.env.ProgramFiles && path.win32.join(process.env.ProgramFiles, 'Git', 'cmd'),
|
|
114
150
|
process.env.LOCALAPPDATA && path.win32.join(process.env.LOCALAPPDATA, 'Programs', 'Git', 'cmd'),
|
|
151
|
+
// JDK auto-installed for keytool (Android SHA-1) and Android builds — its bin
|
|
152
|
+
// lives under a versioned folder, so resolve it dynamically.
|
|
153
|
+
findWindowsJdkBin(),
|
|
115
154
|
].filter(Boolean);
|
|
116
155
|
return candidates.filter((dir) => fs.existsSync(dir));
|
|
117
156
|
}
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* but do it for the user so `kasy new` can prepare a bare machine end to end:
|
|
6
6
|
*
|
|
7
7
|
* 1. Install Git via winget if missing (Flutter refuses to run without it).
|
|
8
|
+
* 1b. Install Microsoft OpenJDK if `keytool` is missing — needed for the
|
|
9
|
+
* Android debug SHA-1 (Google Sign-In) and for Gradle Android builds.
|
|
8
10
|
* 2. Download the current stable Flutter SDK zip and unzip it into
|
|
9
11
|
* %LOCALAPPDATA%\flutter (a per-user dir that never needs admin rights).
|
|
10
12
|
* 3. Persist %LOCALAPPDATA%\flutter\bin on the User PATH for future terminals.
|
|
@@ -62,6 +64,17 @@ if (-not (Get-Command git -ErrorAction SilentlyContinue)) {
|
|
|
62
64
|
throw 'Git is required for Flutter but is not available. Install Git and run this again.'
|
|
63
65
|
}
|
|
64
66
|
|
|
67
|
+
# 1b. JDK — provides 'keytool' (used to register the Android debug SHA-1 for
|
|
68
|
+
# Google Sign-In) and is required by Gradle to build the Android app. A bare
|
|
69
|
+
# Windows machine has neither, so install Microsoft's OpenJDK 17 (free, no
|
|
70
|
+
# account). Non-fatal: if it fails, SHA-1 just falls back to a manual step.
|
|
71
|
+
if (-not (Get-Command keytool -ErrorAction SilentlyContinue)) {
|
|
72
|
+
if (Get-Command winget -ErrorAction SilentlyContinue) {
|
|
73
|
+
winget install --id Microsoft.OpenJDK.17 -e --source winget --silent --accept-source-agreements --accept-package-agreements --disable-interactivity | Out-Null
|
|
74
|
+
Sync-Path
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
65
78
|
# 2. Flutter SDK — download + unzip, unless it's already there.
|
|
66
79
|
if (-not (Test-Path (Join-Path $bin 'flutter.bat'))) {
|
|
67
80
|
if (Test-Path $dest) { Remove-Item -Recurse -Force $dest }
|
|
@@ -136,4 +149,49 @@ function installFlutterWindows({ timeout = 1_800_000 } = {}) {
|
|
|
136
149
|
});
|
|
137
150
|
}
|
|
138
151
|
|
|
139
|
-
|
|
152
|
+
// Standalone JDK installer, for the case where Flutter is ALREADY present (so the
|
|
153
|
+
// installer above never runs) but there's still no JDK — e.g. a machine set up
|
|
154
|
+
// before we started shipping the JDK, hitting the Android SHA-1 step. keytool
|
|
155
|
+
// comes with the JDK; without it Google Sign-In on Android can't be wired up.
|
|
156
|
+
const JDK_PS_SCRIPT = `
|
|
157
|
+
$ErrorActionPreference = 'Stop'
|
|
158
|
+
$ProgressPreference = 'SilentlyContinue'
|
|
159
|
+
if (Get-Command keytool -ErrorAction SilentlyContinue) { Write-Output 'JDK_PRESENT'; exit 0 }
|
|
160
|
+
if (Get-Command winget -ErrorAction SilentlyContinue) {
|
|
161
|
+
winget install --id Microsoft.OpenJDK.17 -e --source winget --silent --accept-source-agreements --accept-package-agreements --disable-interactivity | Out-Null
|
|
162
|
+
}
|
|
163
|
+
Write-Output 'JDK_DONE'
|
|
164
|
+
`;
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Install Microsoft OpenJDK on Windows if `keytool` isn't available. Best-effort
|
|
168
|
+
* and never throws — the caller (SHA-1 registration) degrades to a manual step
|
|
169
|
+
* if it fails. Resolves to the same shape as installFlutterWindows.
|
|
170
|
+
*
|
|
171
|
+
* @param {{ timeout?: number }} [opts]
|
|
172
|
+
* @returns {Promise<{ ok: boolean, stdout: string, stderr: string, error: string|null }>}
|
|
173
|
+
*/
|
|
174
|
+
function ensureJdkWindows({ timeout = 600_000 } = {}) {
|
|
175
|
+
return new Promise((resolve) => {
|
|
176
|
+
if (!isWindows) {
|
|
177
|
+
resolve({ ok: false, stdout: '', stderr: '', error: 'not windows' });
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const encoded = Buffer.from(JDK_PS_SCRIPT, 'utf16le').toString('base64');
|
|
181
|
+
const cmd = `powershell -NoProfile -ExecutionPolicy Bypass -EncodedCommand ${encoded}`;
|
|
182
|
+
exec(
|
|
183
|
+
cmd,
|
|
184
|
+
{ timeout, maxBuffer: 50 * 1024 * 1024, windowsHide: true },
|
|
185
|
+
(error, stdout, stderr) => {
|
|
186
|
+
resolve({
|
|
187
|
+
ok: !error,
|
|
188
|
+
stdout: stdout || '',
|
|
189
|
+
stderr: stderr || '',
|
|
190
|
+
error: error ? error.message : null,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
);
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
module.exports = { installFlutterWindows, flutterWinHome, ensureJdkWindows };
|
|
@@ -134,6 +134,7 @@ module.exports = {
|
|
|
134
134
|
'checks.thenRun': 'Then run',
|
|
135
135
|
'checks.recheck': 'After installing {name}, press Enter to check again',
|
|
136
136
|
'checks.install.confirm': 'Install {name} now? ({cmd})',
|
|
137
|
+
'checks.install.autoInstalling': '{name} is required and is not installed yet. Installing it automatically now ({cmd}).',
|
|
137
138
|
'checks.install.flutterWinDesc': 'downloads the official Flutter SDK + Git, ~1.8 GB',
|
|
138
139
|
'checks.install.flutterWinNote': 'This can take a few minutes (large download). If a Windows approval popup appears, click Yes.',
|
|
139
140
|
'checks.install.running': 'Installing {name}…',
|
|
@@ -238,7 +239,7 @@ module.exports = {
|
|
|
238
239
|
'new.firebase.create.installAfter': 'Then log in',
|
|
239
240
|
'new.firebase.create.installUrl': 'Or download from',
|
|
240
241
|
'new.firebase.create.authCommand': 'Run: gcloud auth login',
|
|
241
|
-
'new.firebase.create.gcloudMissing': 'The Google Cloud CLI (gcloud) is required to create the Firebase project from scratch, and it is not installed yet.',
|
|
242
|
+
'new.firebase.create.gcloudMissing': 'The Google Cloud CLI (gcloud) is required to create the Firebase project from scratch, and it is not installed yet. Installing it automatically now.',
|
|
242
243
|
'new.firebase.create.gcloudInstallConfirm': 'Install the Google Cloud CLI (gcloud) automatically now?',
|
|
243
244
|
'new.firebase.create.gcloudInstalling': 'Installing the Google Cloud CLI…',
|
|
244
245
|
'new.firebase.create.gcloudAuthOpening': 'Opening the Google sign-in in your browser — log in with your account…',
|
|
@@ -134,6 +134,7 @@ module.exports = {
|
|
|
134
134
|
'checks.thenRun': 'Luego ejecuta',
|
|
135
135
|
'checks.recheck': 'Tras instalar {name}, presiona Enter para verificar de nuevo',
|
|
136
136
|
'checks.install.confirm': '¿Instalar {name} ahora? ({cmd})',
|
|
137
|
+
'checks.install.autoInstalling': '{name} es necesario y aún no está instalado. Lo instalaré automáticamente ahora ({cmd}).',
|
|
137
138
|
'checks.install.flutterWinDesc': 'descarga el SDK oficial de Flutter + Git, ~1,8 GB',
|
|
138
139
|
'checks.install.flutterWinNote': 'Esto puede tardar unos minutos (descarga grande). Si aparece una ventana de permiso de Windows, haz clic en Sí.',
|
|
139
140
|
'checks.install.running': 'Instalando {name}…',
|
|
@@ -238,7 +239,7 @@ module.exports = {
|
|
|
238
239
|
'new.firebase.create.installAfter': 'Luego inicia sesión',
|
|
239
240
|
'new.firebase.create.installUrl': 'O descarga en',
|
|
240
241
|
'new.firebase.create.authCommand': 'Ejecuta: gcloud auth login',
|
|
241
|
-
'new.firebase.create.gcloudMissing': 'El Google Cloud CLI (gcloud) es necesario para crear el proyecto Firebase desde cero, y aún no está instalado.',
|
|
242
|
+
'new.firebase.create.gcloudMissing': 'El Google Cloud CLI (gcloud) es necesario para crear el proyecto Firebase desde cero, y aún no está instalado. Lo instalaré automáticamente ahora.',
|
|
242
243
|
'new.firebase.create.gcloudInstallConfirm': '¿Instalar el Google Cloud CLI (gcloud) automáticamente ahora?',
|
|
243
244
|
'new.firebase.create.gcloudInstalling': 'Instalando el Google Cloud CLI…',
|
|
244
245
|
'new.firebase.create.gcloudAuthOpening': 'Abriendo el inicio de sesión de Google en el navegador — entra con tu cuenta…',
|
|
@@ -134,6 +134,7 @@ module.exports = {
|
|
|
134
134
|
'checks.thenRun': 'Depois rode',
|
|
135
135
|
'checks.recheck': 'Após instalar {name}, pressione Enter para verificar novamente',
|
|
136
136
|
'checks.install.confirm': 'Instalar {name} agora? ({cmd})',
|
|
137
|
+
'checks.install.autoInstalling': '{name} é necessário e ainda não está instalado. Vou instalar automaticamente agora ({cmd}).',
|
|
137
138
|
'checks.install.flutterWinDesc': 'baixa o SDK oficial do Flutter + Git, ~1,8 GB',
|
|
138
139
|
'checks.install.flutterWinNote': 'Pode levar alguns minutos (download grande). Se aparecer um popup do Windows pedindo permissão, clique em Sim.',
|
|
139
140
|
'checks.install.running': 'Instalando {name}…',
|
|
@@ -238,7 +239,7 @@ module.exports = {
|
|
|
238
239
|
'new.firebase.create.installAfter': 'Depois faca login',
|
|
239
240
|
'new.firebase.create.installUrl': 'Ou baixe em',
|
|
240
241
|
'new.firebase.create.authCommand': 'Execute: gcloud auth login',
|
|
241
|
-
'new.firebase.create.gcloudMissing': 'O Google Cloud CLI (gcloud) é necessário para criar o projeto Firebase do zero, e ainda não está instalado.',
|
|
242
|
+
'new.firebase.create.gcloudMissing': 'O Google Cloud CLI (gcloud) é necessário para criar o projeto Firebase do zero, e ainda não está instalado. Vou instalar automaticamente agora.',
|
|
242
243
|
'new.firebase.create.gcloudInstallConfirm': 'Instalar o Google Cloud CLI (gcloud) automaticamente agora?',
|
|
243
244
|
'new.firebase.create.gcloudInstalling': 'Instalando o Google Cloud CLI…',
|
|
244
245
|
'new.firebase.create.gcloudAuthOpening': 'Abrindo o login do Google no navegador — entre com a sua conta…',
|
package/package.json
CHANGED
|
@@ -104,6 +104,14 @@ class _WebDevicePreviewState extends State<WebDevicePreview>
|
|
|
104
104
|
late final ValueNotifier<AppLocale> _localeNotifier;
|
|
105
105
|
final _screenshotKey = GlobalKey();
|
|
106
106
|
|
|
107
|
+
// Keeps the wrapped app's Element alive across enable/disable. Toggling the
|
|
108
|
+
// preview moves MaterialApp.router between tree positions (in/out of the
|
|
109
|
+
// device frame); without a stable GlobalKey the Router would be rebuilt from
|
|
110
|
+
// scratch and navigation would snap back to the initial route ('/'). The
|
|
111
|
+
// GlobalKey makes Flutter reparent the same Element, preserving the current
|
|
112
|
+
// screen.
|
|
113
|
+
final _appKey = GlobalKey();
|
|
114
|
+
|
|
107
115
|
static const _textScaleSteps = [1.0, 1.3, 1.5];
|
|
108
116
|
|
|
109
117
|
List<DeviceInfo> get _devices => switch (_platform) {
|
|
@@ -410,7 +418,7 @@ class _WebDevicePreviewState extends State<WebDevicePreview>
|
|
|
410
418
|
deviceNotifier: _deviceNotifier,
|
|
411
419
|
frameVisibleNotifier: _frameVisibleNotifier,
|
|
412
420
|
landscapeNotifier: _landscapeNotifier,
|
|
413
|
-
child: widget.child,
|
|
421
|
+
child: KeyedSubtree(key: _appKey, child: widget.child),
|
|
414
422
|
),
|
|
415
423
|
),
|
|
416
424
|
),
|