kasy-cli 1.21.0 → 1.21.2

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: {
@@ -88,7 +92,16 @@ const PLATFORM_CHECKS = {
88
92
  linux: []
89
93
  };
90
94
 
91
- /** Firebase CLI + FlutterFire — required for push notifications (FCM) on all backends */
95
+ /**
96
+ * Firebase CLI + FlutterFire — required for push notifications (FCM) on EVERY
97
+ * backend (Firebase, Supabase, API), since FCM is the shared push layer.
98
+ *
99
+ * gcloud is deliberately NOT in this list. It's only needed to create a Firebase
100
+ * project FROM SCRATCH (an opt-in path), so checking it for every backend made
101
+ * the Supabase/API setup offer to install gcloud for no reason — and the user
102
+ * doesn't even need it there. The create-from-scratch flow verifies gcloud on
103
+ * its own (checkGcloudAuth in new.js), exactly when it's actually required.
104
+ */
92
105
  const FIREBASE_CHECKS = [
93
106
  {
94
107
  name: 'Firebase CLI',
@@ -109,22 +122,6 @@ const FIREBASE_CHECKS = [
109
122
  pubGlobalBin: 'flutterfire',
110
123
  tryInstallMessageKey: 'setup.flutterfire.installing',
111
124
  },
112
- {
113
- name: 'gcloud CLI (create-from-scratch)',
114
- command: 'gcloud --version',
115
- required: false,
116
- installGuide: () => getInstallGuide('gcloud'),
117
- confirmInstall: true,
118
- waitPromptKey: 'checks.waitPrompt.gcloud.install',
119
- },
120
- {
121
- name: 'gcloud auth (create-from-scratch)',
122
- command: 'gcloud auth print-access-token',
123
- required: false,
124
- showVersion: false,
125
- failHint: 'gcloud auth login',
126
- waitPromptKey: 'checks.waitPrompt.gcloud.auth',
127
- },
128
125
  ];
129
126
 
130
127
  const BACKEND_CHECKS = {
@@ -231,6 +228,7 @@ async function runSingleCheck(check, options = {}) {
231
228
  // Lightweight auto-install (npm / pub global). Heavy tools (gcloud, flutter)
232
229
  // use confirmInstall and are handled interactively after the spinner.
233
230
  if (check.tryInstall) {
231
+ if (typeof options.onInstalling === 'function') options.onInstalling(check);
234
232
  const installed = await execTool(check.tryInstall, INSTALL_TIMEOUT);
235
233
  if (installed.ok) {
236
234
  const verified = await verifyTool(check);
@@ -267,8 +265,7 @@ async function revalidate(check, t) {
267
265
  * Returns true if the tool is present by the end.
268
266
  */
269
267
  async function recoverCheckInteractively(check, t) {
270
- ui.log.warn(`${check.name} ${t('checks.notFound.short') || 'not found'}`);
271
-
268
+ // The step list already flagged this tool as missing; go straight to fixing it.
272
269
  const guide = typeof check.installGuide === 'function' ? check.installGuide() : null;
273
270
 
274
271
  // 1) Offer auto-install for heavy tools that have a package-manager command.
@@ -278,12 +275,17 @@ async function recoverCheckInteractively(check, t) {
278
275
  initialValue: true,
279
276
  });
280
277
  if (doInstall) {
281
- const spinner = ui.spinner();
278
+ const spinner = ui.timedSpinner();
282
279
  spinner.start(t('checks.install.running', { name: check.name }));
283
280
  const installed = await execTool(guide.cmd, INSTALL_TIMEOUT);
284
281
  spinner.stop(t('checks.install.running', { name: check.name }));
285
282
  if (installed.ok && (await revalidate(check, t))) return true;
286
283
  ui.log.warn(t('checks.install.failedManual', { name: check.name }));
284
+ // Surface the installer's own last line — it usually says WHY (needs admin,
285
+ // agreement not accepted, package not found), which beats a generic failure.
286
+ const reason = (installed.stderr || installed.error || '')
287
+ .split('\n').map((l) => l.trim()).filter(Boolean).pop();
288
+ if (reason) ui.log.message(kleur.dim(reason.slice(0, 200)));
287
289
  }
288
290
  }
289
291
 
@@ -350,32 +352,38 @@ async function runChecks(checks, title, options = {}) {
350
352
  // command never blocks waiting for input that will never come.
351
353
  const interactive = options.interactive !== false && Boolean(process.stdout.isTTY);
352
354
 
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
-
355
+ // One step per tool, each with its own running clock — so the user always
356
+ // sees WHAT is being checked or installed, and that it's still moving. This
357
+ // replaces the single frozen "…" spinner that looked stuck during long
358
+ // installs (e.g. FlutterFire). Same stepper the kasy-new flow uses, so the
359
+ // environment setup feels as guided as the project generation, on every OS.
360
+ const stepper = ui.makeTimedStepper();
359
361
  const results = [];
362
+
360
363
  for (const check of checks) {
361
- results.push(await runSingleCheck({ ...check, t }, { showVersion }));
364
+ stepper.next(t('checks.checking', { name: check.name }));
365
+ const result = await runSingleCheck({ ...check, t }, {
366
+ showVersion,
367
+ // Switch the line to "Installing X…" while the auto-install runs, so the
368
+ // clock keeps ticking against a message that explains the wait.
369
+ onInstalling: (c) => stepper.update(
370
+ c.tryInstallMessageKey ? t(c.tryInstallMessageKey) : t('setup.installingNamed', { name: c.name }),
371
+ ),
372
+ });
373
+ results.push(result);
374
+
375
+ if (result.ok) {
376
+ stepper.succeed(result.version ? `${result.name} — ${result.version}` : result.name);
377
+ } else if (result.required) {
378
+ const detail = result.autoInstallFailed ? ` — ${t('checks.install.failed')}` : '';
379
+ stepper.fail(`${t('checks.missing', { name: result.name })}${detail}`);
380
+ } else {
381
+ stepper.warn(t('checks.notFound', { name: result.name }));
382
+ }
362
383
  }
363
384
 
364
385
  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
- }
386
+ if (failures.length === 0) return results;
379
387
 
380
388
  for (const result of failures) {
381
389
  if (interactive && canRecover(result)) {
@@ -383,8 +391,10 @@ async function runChecks(checks, title, options = {}) {
383
391
  if (recovered) {
384
392
  const idx = results.indexOf(result);
385
393
  if (idx >= 0) results[idx] = { ...result, ok: true, autoInstallFailed: false };
386
- continue;
387
394
  }
395
+ // recoverCheckInteractively already printed the outcome/guidance — don't
396
+ // duplicate it. The step line above already showed the missing status.
397
+ continue;
388
398
  }
389
399
  printCheckFailure(result, t);
390
400
  }
@@ -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.2",
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"