kasy-cli 1.21.0 → 1.21.1

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.
@@ -32,7 +32,11 @@ function getInstallGuide(tool) {
32
32
  const guides = {
33
33
  gcloud: {
34
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' },
35
+ // The accept flags are required for a NON-interactive winget run without
36
+ // them winget stalls waiting for the user to accept the source/package
37
+ // agreements (which is why the silent auto-install failed). They're also
38
+ // harmless when the user runs the command by hand.
39
+ win32: { cmd: 'winget install --id Google.CloudSDK -e --accept-source-agreements --accept-package-agreements', after: 'gcloud auth login', url: 'https://cloud.google.com/sdk/docs/install-sdk#windows' },
36
40
  linux: { cmd: 'curl https://sdk.cloud.google.com | bash', after: 'gcloud auth login', url: 'https://cloud.google.com/sdk/docs/install' },
37
41
  },
38
42
  flutter: {
@@ -231,6 +235,7 @@ async function runSingleCheck(check, options = {}) {
231
235
  // Lightweight auto-install (npm / pub global). Heavy tools (gcloud, flutter)
232
236
  // use confirmInstall and are handled interactively after the spinner.
233
237
  if (check.tryInstall) {
238
+ if (typeof options.onInstalling === 'function') options.onInstalling(check);
234
239
  const installed = await execTool(check.tryInstall, INSTALL_TIMEOUT);
235
240
  if (installed.ok) {
236
241
  const verified = await verifyTool(check);
@@ -267,8 +272,7 @@ async function revalidate(check, t) {
267
272
  * Returns true if the tool is present by the end.
268
273
  */
269
274
  async function recoverCheckInteractively(check, t) {
270
- ui.log.warn(`${check.name} ${t('checks.notFound.short') || 'not found'}`);
271
-
275
+ // The step list already flagged this tool as missing; go straight to fixing it.
272
276
  const guide = typeof check.installGuide === 'function' ? check.installGuide() : null;
273
277
 
274
278
  // 1) Offer auto-install for heavy tools that have a package-manager command.
@@ -278,12 +282,17 @@ async function recoverCheckInteractively(check, t) {
278
282
  initialValue: true,
279
283
  });
280
284
  if (doInstall) {
281
- const spinner = ui.spinner();
285
+ const spinner = ui.timedSpinner();
282
286
  spinner.start(t('checks.install.running', { name: check.name }));
283
287
  const installed = await execTool(guide.cmd, INSTALL_TIMEOUT);
284
288
  spinner.stop(t('checks.install.running', { name: check.name }));
285
289
  if (installed.ok && (await revalidate(check, t))) return true;
286
290
  ui.log.warn(t('checks.install.failedManual', { name: check.name }));
291
+ // Surface the installer's own last line — it usually says WHY (needs admin,
292
+ // agreement not accepted, package not found), which beats a generic failure.
293
+ const reason = (installed.stderr || installed.error || '')
294
+ .split('\n').map((l) => l.trim()).filter(Boolean).pop();
295
+ if (reason) ui.log.message(kleur.dim(reason.slice(0, 200)));
287
296
  }
288
297
  }
289
298
 
@@ -350,32 +359,38 @@ async function runChecks(checks, title, options = {}) {
350
359
  // command never blocks waiting for input that will never come.
351
360
  const interactive = options.interactive !== false && Boolean(process.stdout.isTTY);
352
361
 
353
- // Single spinner over all checks, show failures afterwards. The visual
354
- // sits inside the clack rail (│) opened by the caller's ui.intro().
355
- const { spinnerLabel = title, doneLabel = title } = options;
356
- const spinner = ui.spinner();
357
- spinner.start(spinnerLabel);
358
-
362
+ // One step per tool, each with its own running clock — so the user always
363
+ // sees WHAT is being checked or installed, and that it's still moving. This
364
+ // replaces the single frozen "…" spinner that looked stuck during long
365
+ // installs (e.g. FlutterFire). Same stepper the kasy-new flow uses, so the
366
+ // environment setup feels as guided as the project generation, on every OS.
367
+ const stepper = ui.makeTimedStepper();
359
368
  const results = [];
369
+
360
370
  for (const check of checks) {
361
- results.push(await runSingleCheck({ ...check, t }, { showVersion }));
371
+ stepper.next(t('checks.checking', { name: check.name }));
372
+ const result = await runSingleCheck({ ...check, t }, {
373
+ showVersion,
374
+ // Switch the line to "Installing X…" while the auto-install runs, so the
375
+ // clock keeps ticking against a message that explains the wait.
376
+ onInstalling: (c) => stepper.update(
377
+ c.tryInstallMessageKey ? t(c.tryInstallMessageKey) : t('setup.installingNamed', { name: c.name }),
378
+ ),
379
+ });
380
+ results.push(result);
381
+
382
+ if (result.ok) {
383
+ stepper.succeed(result.version ? `${result.name} — ${result.version}` : result.name);
384
+ } else if (result.required) {
385
+ const detail = result.autoInstallFailed ? ` — ${t('checks.install.failed')}` : '';
386
+ stepper.fail(`${t('checks.missing', { name: result.name })}${detail}`);
387
+ } else {
388
+ stepper.warn(t('checks.notFound', { name: result.name }));
389
+ }
362
390
  }
363
391
 
364
392
  const failures = results.filter((r) => !r.ok);
365
- const requiredFailures = failures.filter((r) => r.required);
366
-
367
- if (failures.length === 0) {
368
- spinner.stop(doneLabel);
369
- return results;
370
- }
371
-
372
- // Close the spinner reflecting what actually happened: red ▲ if a required
373
- // check failed, default green ✦ if only optional checks failed (warnings).
374
- if (requiredFailures.length > 0) {
375
- spinner.error(doneLabel);
376
- } else {
377
- spinner.stop(doneLabel);
378
- }
393
+ if (failures.length === 0) return results;
379
394
 
380
395
  for (const result of failures) {
381
396
  if (interactive && canRecover(result)) {
@@ -383,8 +398,10 @@ async function runChecks(checks, title, options = {}) {
383
398
  if (recovered) {
384
399
  const idx = results.indexOf(result);
385
400
  if (idx >= 0) results[idx] = { ...result, ok: true, autoInstallFailed: false };
386
- continue;
387
401
  }
402
+ // recoverCheckInteractively already printed the outcome/guidance — don't
403
+ // duplicate it. The step line above already showed the missing status.
404
+ continue;
388
405
  }
389
406
  printCheckFailure(result, t);
390
407
  }
@@ -70,14 +70,31 @@ function pubCacheBin(name) {
70
70
  }
71
71
 
72
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.
73
+ * Well-known install dirs of tools that may not be on the frozen session PATH
74
+ * yet e.g. the Google Cloud SDK right after it's installed mid-run. Only
75
+ * returns dirs that actually exist. Windows-focused (on Unix these tools land
76
+ * on PATH via the shell profile, which a new terminal already picks up).
77
+ */
78
+ function extraToolDirs() {
79
+ if (!isWindows) return [];
80
+ const candidates = [
81
+ process.env.LOCALAPPDATA && path.win32.join(process.env.LOCALAPPDATA, 'Google', 'Cloud SDK', 'google-cloud-sdk', 'bin'),
82
+ process.env.ProgramFiles && path.win32.join(process.env.ProgramFiles, 'Google', 'Cloud SDK', 'google-cloud-sdk', 'bin'),
83
+ process.env['ProgramFiles(x86)'] && path.win32.join(process.env['ProgramFiles(x86)'], 'Google', 'Cloud SDK', 'google-cloud-sdk', 'bin'),
84
+ ].filter(Boolean);
85
+ return candidates.filter((dir) => fs.existsSync(dir));
86
+ }
87
+
88
+ /**
89
+ * A copy of process.env with `extraDirs` (plus the pub-cache bin and known tool
90
+ * dirs) prepended to PATH, so child processes can find tools installed earlier
91
+ * in this same run. On Windows we set both `PATH` and `Path` because Node reads
92
+ * the original key.
76
93
  *
77
94
  * @param {string[]} [extraDirs] additional directories to expose
78
95
  */
79
96
  function augmentedEnv(extraDirs = []) {
80
- const dirs = [pubCacheBinDir(), ...extraDirs].filter(Boolean);
97
+ const dirs = [pubCacheBinDir(), ...extraToolDirs(), ...extraDirs].filter(Boolean);
81
98
  const env = { ...process.env };
82
99
  // Windows env keys are case-insensitive; find whichever key holds PATH.
83
100
  const pathKey = Object.keys(env).find((k) => k.toLowerCase() === 'path') || 'PATH';
@@ -73,7 +73,8 @@ module.exports = {
73
73
  'setup.checks.backend': 'Backend checks ({backend})',
74
74
  'setup.firebase.installing': 'Installing Firebase CLI...',
75
75
  'setup.supabase.installing': 'Installing Supabase CLI...',
76
- 'setup.flutterfire.installing': 'Installing FlutterFire CLI...',
76
+ 'setup.flutterfire.installing': 'Installing FlutterFire CLI… (may take 1-2 min)',
77
+ 'setup.installingNamed': 'Installing {name}…',
77
78
  'setup.warn.hang': 'If setup stalls, run manually: flutterfire --version',
78
79
  'setup.warn.supabase': 'Using Supabase or custom API? Firebase still helps with push notifications and remote config.',
79
80
  'doctor.title': 'Kasy Doctor',
@@ -73,7 +73,8 @@ module.exports = {
73
73
  'setup.checks.backend': 'Verificaciones de backend ({backend})',
74
74
  'setup.firebase.installing': 'Instalando Firebase CLI...',
75
75
  'setup.supabase.installing': 'Instalando Supabase CLI...',
76
- 'setup.flutterfire.installing': 'Instalando FlutterFire CLI...',
76
+ 'setup.flutterfire.installing': 'Instalando FlutterFire CLI… (puede tardar 1-2 min)',
77
+ 'setup.installingNamed': 'Instalando {name}…',
77
78
  'setup.warn.hang': 'Si se cuelga, ejecuta manualmente: flutterfire --versión',
78
79
  'setup.warn.supabase': '¿Usas Supabase o API propia? Firebase sigue siendo útil para push y remote config.',
79
80
  'doctor.title': 'Kasy Doctor',
@@ -73,7 +73,8 @@ module.exports = {
73
73
  'setup.checks.backend': 'Verificações de backend ({backend})',
74
74
  'setup.firebase.installing': 'Instalando Firebase CLI...',
75
75
  'setup.supabase.installing': 'Instalando Supabase CLI...',
76
- 'setup.flutterfire.installing': 'Instalando FlutterFire CLI...',
76
+ 'setup.flutterfire.installing': 'Instalando FlutterFire CLI… (pode levar 1-2 min)',
77
+ 'setup.installingNamed': 'Instalando {name}…',
77
78
  'setup.warn.hang': 'Se travar, execute manualmente: flutterfire --version',
78
79
  'setup.warn.supabase': 'Usando Supabase ou API propria? O Firebase ainda ajuda com push e remote config.',
79
80
  'doctor.title': 'Kasy Doctor',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kasy-cli",
3
- "version": "1.21.0",
3
+ "version": "1.21.1",
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"