kasy-cli 1.26.0 → 1.28.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.
@@ -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
@@ -205,33 +251,40 @@ async function confirmIdentities(backend, gcloudAccount, tr, cancel) {
205
251
  * id elsewhere). Never throws.
206
252
  */
207
253
  async function ensureGcloudReady(tr) {
208
- const { spawnSync } = require('node:child_process');
254
+ const { spawnSync, exec } = require('node:child_process');
209
255
  const { augmentedEnv } = require('../utils/env-tools');
210
256
 
211
257
  let check = await checkGcloudAuth();
212
258
  if (check.ok) return check;
213
259
 
214
- // 1) Not installed → offer to install it for them.
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
- const doInstall = await ui.confirm({
219
- message: tr('new.firebase.create.gcloudInstallConfirm'),
220
- initialValue: true,
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
+ );
221
279
  });
222
- if (doInstall) {
223
- const spinner = ui.timedSpinner();
224
- spinner.start(tr('new.firebase.create.gcloudInstalling'));
225
- spawnSync(guide.install, { stdio: 'pipe', shell: true });
226
- spinner.stop(tr('new.firebase.create.gcloudInstalling'));
227
- // checkGcloudAuth / the later billing+project calls run with the plain
228
- // process PATH, which doesn't see a tool winget just dropped on the
229
- // machine PATH. Inject the known gcloud dir so the rest of the flow works.
230
- const env = augmentedEnv();
231
- if (env.PATH) process.env.PATH = env.PATH;
232
- if (process.platform === 'win32' && env.Path) process.env.Path = env.Path;
233
- check = await checkGcloudAuth();
234
- }
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();
235
288
  }
236
289
  }
237
290
 
@@ -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 2>/dev/null');
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);
@@ -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) Offer auto-install for heavy tools. Most use a package-manager command
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
- const doInstall = await ui.confirm({
286
- message: t('checks.install.confirm', { name: check.name, cmd: cmdLabel }),
287
- initialValue: true,
288
- });
289
- if (doInstall) {
290
- // Heads-up for the custom installer: it's a big download and may pop a UAC.
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)));
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.
@@ -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 and
75
- * Android Studio's bundled JBR before falling back to bare `keytool`.
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
- module.exports = { installFlutterWindows, flutterWinHome };
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,6 +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',
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.',
241
243
  'new.firebase.create.gcloudInstallConfirm': 'Install the Google Cloud CLI (gcloud) automatically now?',
242
244
  'new.firebase.create.gcloudInstalling': 'Installing the Google Cloud CLI…',
243
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,6 +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',
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.',
241
243
  'new.firebase.create.gcloudInstallConfirm': '¿Instalar el Google Cloud CLI (gcloud) automáticamente ahora?',
242
244
  'new.firebase.create.gcloudInstalling': 'Instalando el Google Cloud CLI…',
243
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,6 +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',
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.',
241
243
  'new.firebase.create.gcloudInstallConfirm': 'Instalar o Google Cloud CLI (gcloud) automaticamente agora?',
242
244
  'new.firebase.create.gcloudInstalling': 'Instalando o Google Cloud CLI…',
243
245
  'new.firebase.create.gcloudAuthOpening': 'Abrindo o login do Google no navegador — entre com a sua conta…',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kasy-cli",
3
- "version": "1.26.0",
3
+ "version": "1.28.0",
4
4
  "description": "CLI for scaffolding production-ready Flutter SaaS apps with Firebase, Supabase, or API REST backends.",
5
5
  "bin": {
6
6
  "kasy": "./bin/kasy.js"
@@ -227,28 +227,24 @@ class _FocusableSidebarState extends State<_FocusableSidebar> {
227
227
 
228
228
  @override
229
229
  Widget build(BuildContext context) {
230
- // Ordered so the first Tab reveals the "skip to content" link (order 0),
231
- // then the sidebar (order 1). The header (2) and content (3) come after,
232
- // ordered by the scaffold's OrderedTraversalPolicy. The invisible anchor
233
- // holds the initial focus so that very first Tab lands on the skip link.
230
+ // Reading-order group (NOT an ordered policy): this is the exact structure
231
+ // that made the anchor hold the initial focus. The anchor sits at (0,0) and
232
+ // is skipped by Tab; the skip link is positioned at the very top, so reading
233
+ // order makes it the FIRST Tab stop, then the sidebar items, then (via the
234
+ // scaffold) the header and content. Swapping in an OrderedTraversalPolicy
235
+ // here broke the anchor, so we keep reading order and rely on position.
234
236
  return FocusTraversalGroup(
235
- policy: OrderedTraversalPolicy(),
236
237
  child: Stack(
237
238
  clipBehavior: Clip.none,
238
239
  children: [
239
- FocusTraversalOrder(
240
- order: const NumericFocusOrder(1),
241
- child: FocusTraversalGroup(child: widget.child),
242
- ),
240
+ widget.child,
243
241
  // Zero-size sibling; only holds the initial keyboard focus.
244
242
  Focus(focusNode: _anchor, child: const SizedBox.shrink()),
243
+ // Topmost on screen, so reading order makes it the first Tab stop.
245
244
  Positioned(
246
245
  top: KasySpacing.sm,
247
246
  left: KasySpacing.sm,
248
- child: FocusTraversalOrder(
249
- order: const NumericFocusOrder(0),
250
- child: _SkipToContentLink(onSkip: _skipToContent),
251
- ),
247
+ child: _SkipToContentLink(onSkip: _skipToContent),
252
248
  ),
253
249
  ],
254
250
  ),
@@ -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
  ),