kasy-cli 1.5.2 → 1.6.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/bin/kasy.js CHANGED
@@ -35,6 +35,7 @@ const { getStoredLanguage, setStoredLanguage } = require('../lib/utils/license')
35
35
  const { promptLanguage } = require('../lib/utils/prompts');
36
36
  const { ensureLicenseKey, shouldRequireLicenseForArgv } = require('../lib/utils/license-gate');
37
37
  const { checkForUpdates } = require('../lib/utils/updates');
38
+ const { printCompactHeader } = require('../lib/utils/brand');
38
39
 
39
40
  function createLocalizedHelpConfig(t) {
40
41
  return {
@@ -322,7 +323,8 @@ function buildProgram(language) {
322
323
  .description(t('cli.command.upgrade.description'))
323
324
  .action(() => {
324
325
  const { spawnSync } = require('node:child_process');
325
- console.log('\n' + kleur.cyan(t('cli.command.upgrade.running')) + '\n');
326
+ printCompactHeader(t);
327
+ console.log(kleur.cyan(t('cli.command.upgrade.running')) + '\n');
326
328
  const result = spawnSync('npm', ['install', '-g', 'kasy-cli'], { stdio: 'inherit', shell: true });
327
329
  if (result.status === 0) {
328
330
  console.log('\n' + kleur.green('✓ ' + t('cli.command.upgrade.done')) + '\n');
@@ -339,7 +341,8 @@ function buildProgram(language) {
339
341
  .description(t('cli.command.uninstall.description'))
340
342
  .action(() => {
341
343
  const { spawnSync } = require('node:child_process');
342
- console.log('\n' + kleur.cyan(t('cli.command.uninstall.running')) + '\n');
344
+ printCompactHeader(t);
345
+ console.log(kleur.cyan(t('cli.command.uninstall.running')) + '\n');
343
346
  const result = spawnSync('npm', ['uninstall', '-g', 'kasy-cli'], { stdio: 'inherit', shell: true });
344
347
  if (result.status === 0) {
345
348
  console.log('\n' + kleur.green('✓ ' + t('cli.command.uninstall.done')) + '\n');
@@ -5,8 +5,8 @@ const os = require('node:os');
5
5
  const fs = require('fs-extra');
6
6
  const pkg = require('../../package.json');
7
7
  const kleur = require('kleur');
8
- const prompts = require('prompts');
9
- const oraPackage = require('ora');
8
+ const ui = require('../utils/ui');
9
+ const { printCompactHeader } = require('../utils/brand');
10
10
  const { createTranslator, detectDefaultLanguage } = require('../utils/i18n');
11
11
  const {
12
12
  AVAILABLE_FEATURES,
@@ -21,7 +21,6 @@ const { writeRouter, writeNoOpAnalyticsApi, writeNoOpTrackingApi, writeNoOpAdmin
21
21
  const { toPackageName, buildTokens } = require('../scaffold/backends/firebase/tokens');
22
22
 
23
23
  const execAsync = promisify(exec);
24
- const ora = oraPackage.default || oraPackage;
25
24
 
26
25
  /** URL do endpoint LLM no app (espelha defineUpdates de llm_chat). */
27
26
  function resolveLlmChatEndpoint(answers, kitSetup) {
@@ -317,58 +316,48 @@ const MODULE_META = {
317
316
  },
318
317
  };
319
318
 
319
+ /**
320
+ * Each entry returns a fn (t, cancel) -> Promise<value> that asks the user.
321
+ * Secret-like values (DSNs, API keys, tokens) use ui.password to mask input.
322
+ */
320
323
  const PROMPT_QUESTIONS = {
321
- sentryDsn: (t) => ({
322
- type: 'text',
323
- name: 'sentryDsn',
324
+ sentryDsn: (t, cancel) => ui.password({
324
325
  message: t('add.prompt.sentryDsn'),
325
- initial: '',
326
+ onCancel: cancel,
326
327
  }),
327
- mixpanelToken: (t) => ({
328
- type: 'text',
329
- name: 'mixpanelToken',
328
+ mixpanelToken: (t, cancel) => ui.password({
330
329
  message: t('add.prompt.mixpanelToken'),
331
- initial: '',
330
+ onCancel: cancel,
332
331
  }),
333
- rcAndroidKey: (t) => ({
334
- type: 'text',
335
- name: 'rcAndroidKey',
332
+ // RevenueCat SDK keys ship inside the client binary — not real secrets, plain text input.
333
+ rcAndroidKey: (t, cancel) => ui.text({
336
334
  message: t('add.prompt.rcAndroidKey'),
337
- initial: '',
335
+ onCancel: cancel,
338
336
  }),
339
- rcIosKey: (t) => ({
340
- type: 'text',
341
- name: 'rcIosKey',
337
+ rcIosKey: (t, cancel) => ui.text({
342
338
  message: t('add.prompt.rcIosKey'),
343
- initial: '',
339
+ onCancel: cancel,
344
340
  }),
345
- llmProvider: (t) => ({
346
- type: 'select',
347
- name: 'llmProvider',
341
+ llmProvider: (t, cancel) => ui.select({
348
342
  message: t('add.prompt.llmProvider'),
349
- choices: [
350
- { title: 'OpenAI (gpt-4o-mini)', value: 'openai' },
351
- { title: 'Google Gemini (gemini-1.5-flash)', value: 'gemini' },
343
+ initialValue: 'openai',
344
+ options: [
345
+ { value: 'openai', label: 'OpenAI (gpt-4o-mini)' },
346
+ { value: 'gemini', label: 'Google Gemini (gemini-1.5-flash)' },
352
347
  ],
353
- initial: 0,
348
+ onCancel: cancel,
354
349
  }),
355
- llmSystemPrompt: (t) => ({
356
- type: 'text',
357
- name: 'llmSystemPrompt',
350
+ llmSystemPrompt: (t, cancel) => ui.text({
358
351
  message: t('add.prompt.llmSystemPrompt'),
359
- initial: '',
352
+ onCancel: cancel,
360
353
  }),
361
- llmApiKey: (t) => ({
362
- type: 'text',
363
- name: 'llmApiKey',
354
+ llmApiKey: (t, cancel) => ui.password({
364
355
  message: t('add.prompt.llmApiKey'),
365
- initial: '',
356
+ onCancel: cancel,
366
357
  }),
367
- llmEndpoint: (t) => ({
368
- type: 'text',
369
- name: 'llmEndpoint',
358
+ llmEndpoint: (t, cancel) => ui.text({
370
359
  message: t('add.prompt.llmEndpoint'),
371
- initial: '',
360
+ onCancel: cancel,
372
361
  }),
373
362
  };
374
363
 
@@ -411,7 +400,7 @@ async function postAddLlmChat(projectDir, kitSetup, answers, t) {
411
400
 
412
401
  // 2. Set API key as secret (skip if blank)
413
402
  if (!apiKey) {
414
- console.log(kleur.yellow(`\n ${t('add.llm_chat.skipSecret')}\n`));
403
+ ui.log.warn(t('add.llm_chat.skipSecret'));
415
404
  return { deployOk: false, deployAttempted: false };
416
405
  }
417
406
 
@@ -419,21 +408,23 @@ async function postAddLlmChat(projectDir, kitSetup, answers, t) {
419
408
  const refFlag = backend === 'supabase' && projectRef ? ` --project-ref ${projectRef}` : '';
420
409
 
421
410
  if (backend === 'firebase') {
422
- const spinner = ora(t('add.llm_chat.settingSecret')).start();
411
+ const spinner = ui.spinner();
412
+ spinner.start(t('add.llm_chat.settingSecret'));
423
413
  try {
424
414
  // Write to temp file — avoids trailing newline (echo) and shell injection risks
425
415
  const tmpFile = path.join(os.tmpdir(), `llm_api_key_${Date.now()}.tmp`);
426
416
  await fs.outputFile(tmpFile, apiKey, 'utf8');
427
417
  await execAsync(`firebase functions:secrets:set LLM_API_KEY --data-file="${tmpFile}"`, { cwd: projectDir });
428
418
  await fs.remove(tmpFile);
429
- spinner.succeed(t('add.llm_chat.secretSet'));
419
+ spinner.stop(t('add.llm_chat.secretSet'));
430
420
  secretsOk = true;
431
421
  } catch {
432
- spinner.warn(t('add.llm_chat.secretFailed'));
433
- console.log(kleur.dim(` Run manually: firebase functions:secrets:set LLM_API_KEY`));
422
+ spinner.stop(`⚠ ${t('add.llm_chat.secretFailed')}`);
423
+ ui.log.message(kleur.dim('Run manually: firebase functions:secrets:set LLM_API_KEY'));
434
424
  }
435
425
  } else if (backend === 'supabase') {
436
- const spinner = ora(t('add.llm_chat.settingSecret')).start();
426
+ const spinner = ui.spinner();
427
+ spinner.start(t('add.llm_chat.settingSecret'));
437
428
  // Set LLM_API_KEY, LLM_PROVIDER and LLM_SYSTEM_PROMPT all as Supabase Secrets.
438
429
  // Deployed Edge Functions read from Deno.env.get() = Supabase Secrets, NOT from .env files.
439
430
  const esc = (v) => String(v).replace(/"/g, '\\"');
@@ -451,32 +442,33 @@ async function postAddLlmChat(projectDir, kitSetup, answers, t) {
451
442
  if (r && r.ok === false) allOk = false;
452
443
  }
453
444
  if (allOk) {
454
- spinner.succeed(t('add.llm_chat.secretSet'));
445
+ spinner.stop(t('add.llm_chat.secretSet'));
455
446
  secretsOk = true;
456
447
  } else {
457
- spinner.warn(t('add.llm_chat.secretFailed'));
458
- console.log(kleur.dim(` Run manually: supabase secrets set LLM_API_KEY=YOUR_KEY LLM_PROVIDER=${provider}${refFlag}`));
448
+ spinner.stop(`⚠ ${t('add.llm_chat.secretFailed')}`);
449
+ ui.log.message(kleur.dim(`Run manually: supabase secrets set LLM_API_KEY=YOUR_KEY LLM_PROVIDER=${provider}${refFlag}`));
459
450
  }
460
451
  }
461
452
 
462
453
  // 3. Deploy the LLM function automatically
463
454
  if (backend === 'api') return { deployOk: false, deployAttempted: false };
464
455
 
465
- const deploySpinner = ora(t('add.llm_chat.deploying')).start();
456
+ const deploySpinner = ui.spinner();
457
+ deploySpinner.start(t('add.llm_chat.deploying'));
466
458
  try {
467
459
  if (backend === 'firebase') {
468
460
  await execAsync('firebase deploy --only functions:llmChat', { cwd: projectDir, timeout: 180_000 });
469
461
  } else if (backend === 'supabase') {
470
462
  await execAsync(`supabase functions deploy llm-chat --no-verify-jwt${refFlag}`, { cwd: projectDir, timeout: 180_000 });
471
463
  }
472
- deploySpinner.succeed(t('add.llm_chat.deployed'));
464
+ deploySpinner.stop(t('add.llm_chat.deployed'));
473
465
  return { deployOk: true, deployAttempted: true };
474
466
  } catch {
475
- deploySpinner.warn(t('add.llm_chat.deployFailed'));
467
+ deploySpinner.stop(`⚠ ${t('add.llm_chat.deployFailed')}`);
476
468
  if (backend === 'firebase') {
477
- console.log(kleur.dim(` Run manually: firebase deploy --only functions:llmChat`));
469
+ ui.log.message(kleur.dim('Run manually: firebase deploy --only functions:llmChat'));
478
470
  } else {
479
- console.log(kleur.dim(` Run manually: supabase functions deploy llm-chat --no-verify-jwt${refFlag}`));
471
+ ui.log.message(kleur.dim(`Run manually: supabase functions deploy llm-chat --no-verify-jwt${refFlag}`));
480
472
  }
481
473
  return { deployOk: false, deployAttempted: true };
482
474
  }
@@ -485,8 +477,6 @@ async function postAddLlmChat(projectDir, kitSetup, answers, t) {
485
477
  // ── List command ──────────────────────────────────────────────────────────────
486
478
 
487
479
  async function listModules(projectDir, t) {
488
- console.log(kleur.bold(`\n${t('add.list.title')}\n`));
489
-
490
480
  let activeModules = [];
491
481
  const kitSetupPath = path.join(projectDir, 'kit_setup.json');
492
482
  if (await fs.pathExists(kitSetupPath)) {
@@ -498,14 +488,15 @@ async function listModules(projectDir, t) {
498
488
  }
499
489
  }
500
490
 
491
+ const lines = [];
501
492
  for (const feature of getVisibleFeatures({ audience: KASY_AUDIENCE })) {
502
493
  const active = activeModules.includes(feature.id);
503
494
  const badge = feature.status === 'internal' ? kleur.yellow(' [beta]') : '';
504
495
  const icon = active ? kleur.green('✓') : kleur.dim('○');
505
496
  const label = active ? kleur.green(feature.id) : kleur.white(feature.id);
506
- console.log(` ${icon} ${label}${badge}`);
497
+ lines.push(`${icon} ${label}${badge}`);
507
498
  }
508
- console.log('');
499
+ ui.note(lines.join('\n'), t('add.list.title'));
509
500
  }
510
501
 
511
502
  // ── Main command ──────────────────────────────────────────────────────────────
@@ -513,16 +504,23 @@ async function listModules(projectDir, t) {
513
504
  async function runAdd(module, options = {}) {
514
505
  const t = createTranslator(options.language || detectDefaultLanguage());
515
506
  const projectDir = path.resolve(options.directory || '.');
507
+ const cancel = () => { ui.cancel(t('add.cancelled')); process.exit(0); };
516
508
 
517
509
  // --list flag
518
510
  if (options.list) {
511
+ printCompactHeader(t);
512
+ ui.intro(t('add.list.title'));
519
513
  await listModules(projectDir, t);
514
+ ui.outro('');
520
515
  return;
521
516
  }
522
517
 
523
518
  if (!module) {
524
- console.log(kleur.yellow(t('add.error.noModule')));
519
+ printCompactHeader(t);
520
+ ui.intro(t('add.list.title'));
521
+ ui.log.warn(t('add.error.noModule'));
525
522
  await listModules(projectDir, t);
523
+ ui.outro('');
526
524
  return;
527
525
  }
528
526
 
@@ -542,13 +540,16 @@ async function runAdd(module, options = {}) {
542
540
  if (moduleLower === 'ios-release' || moduleLower === 'ios_release' || moduleLower === 'iosrelease') {
543
541
  const scriptPath = path.join(projectDir, 'scripts', 'release-ios.sh');
544
542
  if (await fs.pathExists(scriptPath)) {
545
- console.log(kleur.yellow(`\n${t('add.iosRelease.already')}\n`));
543
+ printCompactHeader(t);
544
+ ui.log.warn(t('add.iosRelease.already'));
546
545
  return;
547
546
  }
548
547
  const patchDir = path.join(FEATURES_PATCH_DIR, 'ios-release');
549
548
  if (!(await fs.pathExists(patchDir))) {
550
549
  throw new Error(t('add.error.unknownModule', { module, list: 'ios-release' }));
551
550
  }
551
+ printCompactHeader(t);
552
+ ui.intro(t('add.applying', { module: 'ios-release' }));
552
553
  const { tokens, pathReplacements } = buildTokens({
553
554
  appName: kitSetup.appName,
554
555
  bundleId: kitSetup.bundleId,
@@ -557,8 +558,8 @@ async function runAdd(module, options = {}) {
557
558
  await localizeReleaseDocs(projectDir, options.language || detectDefaultLanguage());
558
559
  await ensureIosReleaseMakefile(projectDir);
559
560
  await ensureGitignoreKasyEnv(projectDir);
560
- console.log(kleur.green(`\n✓ ${t('add.iosRelease.success')}\n`));
561
- console.log(kleur.cyan(` kasy ios configure\n`));
561
+ ui.log.success(t('add.iosRelease.success'));
562
+ ui.outro(kleur.cyan('kasy ios configure'));
562
563
  return;
563
564
  }
564
565
 
@@ -578,23 +579,22 @@ async function runAdd(module, options = {}) {
578
579
  if (activeModules.includes(normalized)) {
579
580
  // llm_chat can be re-run to reconfigure credentials even when already active
580
581
  if (normalized !== 'llm_chat') {
581
- console.log(kleur.yellow(`\n${t('add.alreadyActive', { module: normalized })}\n`));
582
+ printCompactHeader(t);
583
+ ui.log.warn(t('add.alreadyActive', { module: normalized }));
582
584
  return;
583
585
  }
584
- console.log(kleur.yellow(`\n${t('add.llm_chat.reconfigure')}\n`));
586
+ printCompactHeader(t);
587
+ ui.intro(t('add.llm_chat.reconfigure'));
585
588
  // Skip to credential-only flow: prompt → postAdd → done
586
589
  const answers = {};
587
590
  if (!options.yes) {
588
- console.log('');
589
591
  for (const key of ['llmProvider', 'llmSystemPrompt', 'llmApiKey']) {
590
- const question = PROMPT_QUESTIONS[key]?.(t);
591
- if (!question) continue;
592
- const response = await prompts(question, { onCancel: () => { throw new Error(t('add.cancelled')); } });
593
- answers[key] = response[key] ?? '';
592
+ const ask = PROMPT_QUESTIONS[key];
593
+ if (!ask) continue;
594
+ answers[key] = (await ask(t, cancel)) ?? '';
594
595
  }
595
596
  if (kitSetup.backendProvider === 'api') {
596
- const response = await prompts(PROMPT_QUESTIONS.llmEndpoint(t), { onCancel: () => { throw new Error(t('add.cancelled')); } });
597
- answers.llmEndpoint = response.llmEndpoint || '';
597
+ answers.llmEndpoint = (await PROMPT_QUESTIONS.llmEndpoint(t, cancel)) || '';
598
598
  }
599
599
  }
600
600
  const defines = MODULE_META.llm_chat.defineUpdates(answers, kitSetup);
@@ -610,41 +610,37 @@ async function runAdd(module, options = {}) {
610
610
  const { deployOk, deployAttempted } = await postAddLlmChat(projectDir, kitSetup, answers, t);
611
611
  const backend = kitSetup?.backendProvider ?? 'firebase';
612
612
  const needsManualDeploy = deployAttempted && !deployOk;
613
+ let nextStepsMsg;
613
614
  if (backend === 'firebase') {
614
- console.log(kleur.cyan(t(needsManualDeploy ? 'add.llm_chat.nextSteps.firebase.deployFailed' : 'add.llm_chat.nextSteps.firebase')));
615
+ nextStepsMsg = t(needsManualDeploy ? 'add.llm_chat.nextSteps.firebase.deployFailed' : 'add.llm_chat.nextSteps.firebase');
615
616
  } else if (backend === 'supabase') {
616
- console.log(kleur.cyan(t(needsManualDeploy ? 'add.llm_chat.nextSteps.supabase.deployFailed' : 'add.llm_chat.nextSteps.supabase')));
617
+ nextStepsMsg = t(needsManualDeploy ? 'add.llm_chat.nextSteps.supabase.deployFailed' : 'add.llm_chat.nextSteps.supabase');
617
618
  } else {
618
- console.log(kleur.cyan(t('add.llm_chat.nextSteps.api')));
619
+ nextStepsMsg = t('add.llm_chat.nextSteps.api');
619
620
  }
621
+ ui.outro(kleur.cyan(nextStepsMsg));
620
622
  return;
621
623
  }
622
624
 
623
625
  const meta = MODULE_META[normalized] || { promptKeys: [], defineUpdates: () => ({}), envLines: () => [], featureFlag: null };
624
626
 
627
+ printCompactHeader(t);
628
+ ui.intro(t('add.applying', { module: normalized }));
629
+
625
630
  // 4. Prompt for API keys (skip if --yes)
626
631
  const answers = {};
627
632
  if (!options.yes && meta.promptKeys.length > 0) {
628
- console.log('');
629
633
  for (const key of meta.promptKeys) {
630
- const question = PROMPT_QUESTIONS[key]?.(t);
631
- if (!question) continue;
632
- const response = await prompts(question, {
633
- onCancel: () => { throw new Error(t('add.cancelled')); },
634
- });
635
- answers[key] = response[key] ?? '';
634
+ const ask = PROMPT_QUESTIONS[key];
635
+ if (!ask) continue;
636
+ answers[key] = (await ask(t, cancel)) ?? '';
636
637
  }
637
638
  // Extra prompt for API backend (custom LLM endpoint)
638
639
  if (normalized === 'llm_chat' && kitSetup.backendProvider === 'api') {
639
- const response = await prompts(PROMPT_QUESTIONS.llmEndpoint(t), {
640
- onCancel: () => { throw new Error(t('add.cancelled')); },
641
- });
642
- answers.llmEndpoint = response.llmEndpoint || '';
640
+ answers.llmEndpoint = (await PROMPT_QUESTIONS.llmEndpoint(t, cancel)) || '';
643
641
  }
644
642
  }
645
643
 
646
- console.log(kleur.bold(`\n${t('add.applying', { module: normalized })}\n`));
647
-
648
644
  // 5. Enable feature flag in features.dart
649
645
  if (meta.featureFlag) {
650
646
  await enableFeatureFlag(projectDir, meta.featureFlag);
@@ -667,16 +663,17 @@ async function runAdd(module, options = {}) {
667
663
  // 8. Apply patch if it exists under features/<module>/
668
664
  const patchDir = path.join(FEATURES_PATCH_DIR, normalized);
669
665
  if (await fs.pathExists(patchDir)) {
670
- const spinner = ora(t('add.applyingPatch')).start();
666
+ const spinner = ui.spinner();
667
+ spinner.start(t('add.applyingPatch'));
671
668
  try {
672
669
  const { tokens: patchTokens, pathReplacements: patchPathReplacements } = buildTokens({
673
670
  appName: kitSetup.appName,
674
671
  bundleId: kitSetup.bundleId,
675
672
  });
676
673
  await applyPatch(patchDir, projectDir, patchTokens, patchPathReplacements);
677
- spinner.succeed(t('add.patchApplied'));
674
+ spinner.stop(t('add.patchApplied'));
678
675
  } catch (err) {
679
- spinner.fail(t('add.patchFailed'));
676
+ spinner.error(t('add.patchFailed'));
680
677
  throw err;
681
678
  }
682
679
  }
@@ -726,24 +723,26 @@ async function runAdd(module, options = {}) {
726
723
 
727
724
  // 10. flutter pub get
728
725
  {
729
- const spinner = ora(t('add.pubGet')).start();
726
+ const spinner = ui.spinner();
727
+ spinner.start(t('add.pubGet'));
730
728
  try {
731
729
  await execAsync('flutter pub get', { cwd: projectDir, timeout: 300_000 });
732
- spinner.succeed(t('add.pubGetDone'));
730
+ spinner.stop(t('add.pubGetDone'));
733
731
  } catch {
734
- spinner.warn(t('add.pubGetFailed'));
732
+ spinner.stop(`⚠ ${t('add.pubGetFailed')}`);
735
733
  }
736
734
  }
737
735
 
738
736
  // 11. build_runner (only when needed: features with codegen)
739
737
  const needsBuildRunner = ['revenuecat', 'analytics', 'sentry', 'onboarding', 'llm_chat', 'feedback'].includes(normalized);
740
738
  if (needsBuildRunner) {
741
- const spinner = ora(t('add.buildRunner')).start();
739
+ const spinner = ui.spinner();
740
+ spinner.start(t('add.buildRunner'));
742
741
  try {
743
742
  await execAsync('dart run build_runner build --delete-conflicting-outputs', { cwd: projectDir, timeout: 600_000 });
744
- spinner.succeed(t('add.buildRunnerDone'));
743
+ spinner.stop(t('add.buildRunnerDone'));
745
744
  } catch {
746
- spinner.warn(t('add.buildRunnerFailed'));
745
+ spinner.stop(`⚠ ${t('add.buildRunnerFailed')}`);
747
746
  }
748
747
  }
749
748
 
@@ -755,23 +754,25 @@ async function runAdd(module, options = {}) {
755
754
 
756
755
  // Show note if any
757
756
  if (meta.note) {
758
- console.log(kleur.dim(`\n ${t(meta.note)}\n`));
757
+ ui.log.message(kleur.dim(t(meta.note)));
759
758
  }
760
759
 
761
- console.log(kleur.green(`\n✓ ${t('add.success', { module: normalized })}\n`));
762
-
763
760
  // LLM Chat: show next steps (adjust based on whether deploy succeeded)
764
761
  if (normalized === 'llm_chat') {
765
762
  const backend = kitSetup?.backendProvider ?? 'firebase';
766
763
  const needsManualDeploy = llmDeployResult?.deployAttempted && !llmDeployResult?.deployOk;
764
+ let nextStepsMsg;
767
765
  if (backend === 'firebase') {
768
- console.log(kleur.cyan(t(needsManualDeploy ? 'add.llm_chat.nextSteps.firebase.deployFailed' : 'add.llm_chat.nextSteps.firebase')));
766
+ nextStepsMsg = t(needsManualDeploy ? 'add.llm_chat.nextSteps.firebase.deployFailed' : 'add.llm_chat.nextSteps.firebase');
769
767
  } else if (backend === 'supabase') {
770
- console.log(kleur.cyan(t(needsManualDeploy ? 'add.llm_chat.nextSteps.supabase.deployFailed' : 'add.llm_chat.nextSteps.supabase')));
768
+ nextStepsMsg = t(needsManualDeploy ? 'add.llm_chat.nextSteps.supabase.deployFailed' : 'add.llm_chat.nextSteps.supabase');
771
769
  } else {
772
- console.log(kleur.cyan(t('add.llm_chat.nextSteps.api')));
770
+ nextStepsMsg = t('add.llm_chat.nextSteps.api');
773
771
  }
772
+ ui.log.info(nextStepsMsg);
774
773
  }
774
+
775
+ ui.outro(t('add.success', { module: normalized }));
775
776
  }
776
777
 
777
778
  module.exports = { runAdd };
@@ -17,15 +17,16 @@
17
17
  const path = require('node:path');
18
18
  const fs = require('fs-extra');
19
19
  const kleur = require('kleur');
20
- const oraPackage = require('ora');
20
+ const ui = require('../utils/ui');
21
+ const { printCompactHeader } = require('../utils/brand');
21
22
  const { exec } = require('node:child_process');
22
23
  const { promisify } = require('node:util');
23
24
 
24
25
  const { getStoredLanguage } = require('../utils/license');
26
+ const { createTranslator, detectDefaultLanguage } = require('../utils/i18n');
25
27
  const { createFcmServiceAccountKey } = require('../scaffold/shared/fcm-service-account');
26
28
  const { setSupabaseSecrets } = require('../scaffold/backends/supabase/deploy');
27
29
 
28
- const ora = oraPackage.default || oraPackage;
29
30
  const execAsync = promisify(exec);
30
31
 
31
32
  // ── Helpers ───────────────────────────────────────────────────────────────────
@@ -39,10 +40,10 @@ async function runCmd(cmd, cwd) {
39
40
  }
40
41
  }
41
42
 
42
- function ok(label, detail) { console.log(` ${kleur.green('✓')} ${label}${detail ? kleur.gray(` — ${detail}`) : ''}`); }
43
- function fail(label, detail) { console.log(` ${kleur.red('✗')} ${kleur.red(label)}${detail ? kleur.gray(` — ${detail}`) : ''}`); }
44
- function warn(label, detail) { console.log(` ${kleur.yellow('⚠')} ${kleur.yellow(label)}${detail ? kleur.gray(` — ${detail}`) : ''}`); }
45
- function info(label) { console.log(` ${kleur.gray('–')} ${kleur.gray(label)}`); }
43
+ function ok(label, detail) { ui.log.success(`${label}${detail ? kleur.gray(` — ${detail}`) : ''}`); }
44
+ function fail(label, detail) { ui.log.error(`${label}${detail ? kleur.gray(` — ${detail}`) : ''}`); }
45
+ function warn(label, detail) { ui.log.warn(`${label}${detail ? kleur.gray(` — ${detail}`) : ''}`); }
46
+ function info(label) { ui.log.message(kleur.dim(`– ${label}`)); }
46
47
 
47
48
  /**
48
49
  * Read the linked Supabase project ref.
@@ -105,8 +106,11 @@ async function listDeployedFunctions(projectDir) {
105
106
  */
106
107
  async function runCheck(options = {}) {
107
108
  const projectDir = path.resolve(options.directory || '.');
109
+ const lang = options.language || getStoredLanguage() || detectDefaultLanguage();
110
+ const t = createTranslator(lang);
108
111
 
109
- console.log(kleur.bold(`\n Kasy Check — Push Notifications\n`));
112
+ printCompactHeader(t);
113
+ ui.intro('Kasy Check — Push Notifications');
110
114
 
111
115
  // ── Detect backend ────────────────────────────────────────────────────────
112
116
  const isFirebase = await fs.pathExists(path.join(projectDir, 'firebase.json'));
@@ -115,20 +119,22 @@ async function runCheck(options = {}) {
115
119
  if (isFirebase && !isSupabase) {
116
120
  ok('Firebase backend');
117
121
  info('Firebase usa Application Default Credentials — nenhuma configuração extra necessária.');
118
- console.log('');
119
- // APNs Key is still required for iOS push — show reminder even for Firebase backend.
120
122
  const firebaseProjectId = await readFirebaseProjectId(projectDir);
121
- console.log(kleur.bold().yellow(` ⚠ Push iOS requer APNs Key (não verificável via CLI)`));
122
- console.log(kleur.gray(` Firebase Console Cloud Messaging app iOS → Chave de autenticação APNs`));
123
+ const apnsLines = [
124
+ kleur.yellow('Push iOS requer APNs Key (não verificável via CLI)'),
125
+ kleur.dim('Firebase Console → Cloud Messaging → app iOS → Chave de autenticação APNs'),
126
+ ];
123
127
  if (firebaseProjectId) {
124
- console.log(kleur.cyan(` https://console.firebase.google.com/project/${firebaseProjectId}/settings/cloudmessaging`));
128
+ apnsLines.push(kleur.cyan(`https://console.firebase.google.com/project/${firebaseProjectId}/settings/cloudmessaging`));
125
129
  }
126
- console.log('');
130
+ ui.note(apnsLines.join('\n'));
131
+ ui.outro('Done');
127
132
  return;
128
133
  }
129
134
 
130
135
  if (!isSupabase) {
131
- console.error(kleur.red(` ✗ Diretório não parece ser um projeto Kasy.\n`));
136
+ ui.log.error('Diretório não parece ser um projeto Kasy.');
137
+ ui.cancel('Aborted');
132
138
  process.exit(1);
133
139
  }
134
140
 
@@ -136,15 +142,16 @@ async function runCheck(options = {}) {
136
142
  const projectRef = await readProjectRef(projectDir);
137
143
  if (!projectRef) {
138
144
  fail('Projeto Supabase não vinculado', 'execute: supabase link --project-ref SEU_REF');
139
- console.log('');
145
+ ui.cancel('Aborted');
140
146
  process.exit(1);
141
147
  }
142
148
  ok('Projeto vinculado', projectRef);
143
149
 
144
150
  // ── Read secrets list ─────────────────────────────────────────────────────
145
- const spinner = ora(kleur.gray(' Verificando secrets…')).start();
151
+ const spinner = ui.spinner();
152
+ spinner.start('Verificando secrets…');
146
153
  const secrets = await listSecretNames(projectDir);
147
- spinner.stop();
154
+ spinner.stop('Secrets verificados');
148
155
 
149
156
  if (!secrets) {
150
157
  warn('Não foi possível listar secrets', 'verifique: supabase login');
@@ -158,7 +165,7 @@ async function runCheck(options = {}) {
158
165
  } else {
159
166
  fail('FIREBASE_PROJECT_ID ausente');
160
167
  if (firebaseProjectId) {
161
- console.log(kleur.gray(` Corrija: supabase secrets set FIREBASE_PROJECT_ID="${firebaseProjectId}"`));
168
+ ui.log.message(kleur.gray(`Corrija: supabase secrets set FIREBASE_PROJECT_ID="${firebaseProjectId}"`));
162
169
  if (options.fix) {
163
170
  const r = await runCmd(`supabase secrets set FIREBASE_PROJECT_ID="${firebaseProjectId}"`, projectDir);
164
171
  r.ok
@@ -177,9 +184,10 @@ async function runCheck(options = {}) {
177
184
  fail('FIREBASE_SERVICE_ACCOUNT_JSON ausente');
178
185
 
179
186
  if (options.fix && firebaseProjectId) {
180
- const fixSpinner = ora(kleur.cyan(' Gerando e configurando chave FCM…')).start();
187
+ const fixSpinner = ui.spinner();
188
+ fixSpinner.start('Gerando e configurando chave FCM…');
181
189
  const fcmResult = await createFcmServiceAccountKey(firebaseProjectId);
182
- fixSpinner.stop();
190
+ fixSpinner.stop('Chave FCM gerada');
183
191
 
184
192
  if (fcmResult.ok) {
185
193
  const secretSteps = await setSupabaseSecrets(projectDir, {
@@ -192,21 +200,26 @@ async function runCheck(options = {}) {
192
200
  : fail('FIREBASE_SERVICE_ACCOUNT_JSON → falhou ao configurar', setStep?.error);
193
201
  } else {
194
202
  fail('FIREBASE_SERVICE_ACCOUNT_JSON → não foi possível gerar chave', fcmResult.error);
195
- console.log(kleur.gray(` Manual: Firebase Console → Configurações → Contas de serviço → Gerar chave`));
196
- console.log(kleur.gray(` Depois: supabase secrets set FIREBASE_SERVICE_ACCOUNT_JSON='$(cat chave.json)'`));
203
+ ui.log.message(kleur.gray(
204
+ 'Manual: Firebase Console Configurações → Contas de serviço → Gerar chave\n' +
205
+ "Depois: supabase secrets set FIREBASE_SERVICE_ACCOUNT_JSON='$(cat chave.json)'"
206
+ ));
197
207
  }
198
208
  } else {
199
- console.log(kleur.gray(` Corrija automaticamente: kasy check --fix`));
200
- console.log(kleur.gray(` Ou manualmente: Firebase Console Configurações → Contas de serviço → Gerar chave`));
201
- console.log(kleur.gray(` Depois: supabase secrets set FIREBASE_SERVICE_ACCOUNT_JSON='$(cat chave.json)'`));
209
+ ui.log.message(kleur.gray(
210
+ 'Corrija automaticamente: kasy check --fix\n' +
211
+ 'Ou manualmente: Firebase Console Configurações → Contas de serviço → Gerar chave\n' +
212
+ "Depois: supabase secrets set FIREBASE_SERVICE_ACCOUNT_JSON='$(cat chave.json)'"
213
+ ));
202
214
  }
203
215
  }
204
216
  }
205
217
 
206
218
  // ── 4. Edge function send-push-notification ────────────────────────────────
207
- const fnSpinner = ora(kleur.gray(' Verificando edge functions…')).start();
219
+ const fnSpinner = ui.spinner();
220
+ fnSpinner.start('Verificando edge functions…');
208
221
  const functions = await listDeployedFunctions(projectDir);
209
- fnSpinner.stop();
222
+ fnSpinner.stop('Edge functions verificadas');
210
223
 
211
224
  if (!functions) {
212
225
  warn('Não foi possível listar edge functions', 'verifique: supabase login');
@@ -215,26 +228,30 @@ async function runCheck(options = {}) {
215
228
  } else {
216
229
  fail('Edge function send-push-notification não deployada');
217
230
  if (options.fix) {
218
- const deploySpinner = ora(kleur.cyan(' Publicando send-push-notification…')).start();
231
+ const deploySpinner = ui.spinner();
232
+ deploySpinner.start('Publicando send-push-notification…');
219
233
  const r = await runCmd('supabase functions deploy send-push-notification', projectDir);
220
- deploySpinner.stop();
221
234
  r.ok
222
- ? ok('send-push-notification → deployada automaticamente')
223
- : fail('send-push-notification → falhou no deploy', r.error);
235
+ ? deploySpinner.stop('send-push-notification → deployada automaticamente')
236
+ : deploySpinner.error(`send-push-notification → falhou no deploy${r.error ? ` — ${r.error}` : ''}`);
224
237
  } else {
225
- console.log(kleur.gray(` Corrija automaticamente: kasy check --fix`));
226
- console.log(kleur.gray(` Ou manualmente: supabase functions deploy send-push-notification`));
238
+ ui.log.message(kleur.gray(
239
+ 'Corrija automaticamente: kasy check --fix\n' +
240
+ 'Ou manualmente: supabase functions deploy send-push-notification'
241
+ ));
227
242
  }
228
243
  }
229
244
 
230
245
  // ── 5. APNs reminder ─────────────────────────────────────────────────────
231
- console.log('');
232
- console.log(kleur.bold().yellow(` ⚠ Push iOS requer APNs Key (não verificável via CLI)`));
233
- console.log(kleur.gray(` Firebase Console → Cloud Messaging → app iOS → Chave de autenticação APNs`));
246
+ const apnsLines = [
247
+ kleur.yellow('Push iOS requer APNs Key (não verificável via CLI)'),
248
+ kleur.dim('Firebase Console → Cloud Messaging → app iOS → Chave de autenticação APNs'),
249
+ ];
234
250
  if (firebaseProjectId) {
235
- console.log(kleur.cyan(` https://console.firebase.google.com/project/${firebaseProjectId}/settings/cloudmessaging`));
251
+ apnsLines.push(kleur.cyan(`https://console.firebase.google.com/project/${firebaseProjectId}/settings/cloudmessaging`));
236
252
  }
237
- console.log('');
253
+ ui.note(apnsLines.join('\n'));
254
+ ui.outro('Done');
238
255
  }
239
256
 
240
257
  module.exports = { runCheck };