kasy-cli 1.5.3 → 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 +5 -2
- package/lib/commands/add.js +101 -100
- package/lib/commands/check.js +55 -38
- package/lib/commands/codemagic.js +61 -58
- package/lib/commands/deploy.js +49 -45
- package/lib/commands/docs.js +19 -18
- package/lib/commands/doctor.js +46 -44
- package/lib/commands/features.js +20 -17
- package/lib/commands/ios.js +69 -69
- package/lib/commands/new.js +529 -771
- package/lib/commands/notifications.js +59 -59
- package/lib/commands/remove.js +28 -27
- package/lib/commands/run.js +3 -1
- package/lib/commands/update.js +104 -96
- package/lib/commands/validate.js +24 -19
- package/lib/utils/apple-release.js +23 -11
- package/lib/utils/brand.js +72 -0
- package/lib/utils/checks.js +20 -9
- package/lib/utils/prompts.js +82 -142
- package/lib/utils/ui.js +92 -0
- package/lib/utils/updates.js +9 -8
- package/package.json +2 -1
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
|
-
|
|
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
|
-
|
|
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');
|
package/lib/commands/add.js
CHANGED
|
@@ -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
|
|
9
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
330
|
+
onCancel: cancel,
|
|
332
331
|
}),
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
{
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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.
|
|
419
|
+
spinner.stop(t('add.llm_chat.secretSet'));
|
|
430
420
|
secretsOk = true;
|
|
431
421
|
} catch {
|
|
432
|
-
spinner.
|
|
433
|
-
|
|
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 =
|
|
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.
|
|
445
|
+
spinner.stop(t('add.llm_chat.secretSet'));
|
|
455
446
|
secretsOk = true;
|
|
456
447
|
} else {
|
|
457
|
-
spinner.
|
|
458
|
-
|
|
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 =
|
|
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.
|
|
464
|
+
deploySpinner.stop(t('add.llm_chat.deployed'));
|
|
473
465
|
return { deployOk: true, deployAttempted: true };
|
|
474
466
|
} catch {
|
|
475
|
-
deploySpinner.
|
|
467
|
+
deploySpinner.stop(`⚠ ${t('add.llm_chat.deployFailed')}`);
|
|
476
468
|
if (backend === 'firebase') {
|
|
477
|
-
|
|
469
|
+
ui.log.message(kleur.dim('Run manually: firebase deploy --only functions:llmChat'));
|
|
478
470
|
} else {
|
|
479
|
-
|
|
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
|
-
|
|
497
|
+
lines.push(`${icon} ${label}${badge}`);
|
|
507
498
|
}
|
|
508
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
561
|
-
|
|
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
|
-
|
|
582
|
+
printCompactHeader(t);
|
|
583
|
+
ui.log.warn(t('add.alreadyActive', { module: normalized }));
|
|
582
584
|
return;
|
|
583
585
|
}
|
|
584
|
-
|
|
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
|
|
591
|
-
if (!
|
|
592
|
-
|
|
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
|
-
|
|
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
|
-
|
|
615
|
+
nextStepsMsg = t(needsManualDeploy ? 'add.llm_chat.nextSteps.firebase.deployFailed' : 'add.llm_chat.nextSteps.firebase');
|
|
615
616
|
} else if (backend === 'supabase') {
|
|
616
|
-
|
|
617
|
+
nextStepsMsg = t(needsManualDeploy ? 'add.llm_chat.nextSteps.supabase.deployFailed' : 'add.llm_chat.nextSteps.supabase');
|
|
617
618
|
} else {
|
|
618
|
-
|
|
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
|
|
631
|
-
if (!
|
|
632
|
-
|
|
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
|
-
|
|
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 =
|
|
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.
|
|
674
|
+
spinner.stop(t('add.patchApplied'));
|
|
678
675
|
} catch (err) {
|
|
679
|
-
spinner.
|
|
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 =
|
|
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.
|
|
730
|
+
spinner.stop(t('add.pubGetDone'));
|
|
733
731
|
} catch {
|
|
734
|
-
spinner.
|
|
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 =
|
|
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.
|
|
743
|
+
spinner.stop(t('add.buildRunnerDone'));
|
|
745
744
|
} catch {
|
|
746
|
-
spinner.
|
|
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
|
-
|
|
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
|
-
|
|
766
|
+
nextStepsMsg = t(needsManualDeploy ? 'add.llm_chat.nextSteps.firebase.deployFailed' : 'add.llm_chat.nextSteps.firebase');
|
|
769
767
|
} else if (backend === 'supabase') {
|
|
770
|
-
|
|
768
|
+
nextStepsMsg = t(needsManualDeploy ? 'add.llm_chat.nextSteps.supabase.deployFailed' : 'add.llm_chat.nextSteps.supabase');
|
|
771
769
|
} else {
|
|
772
|
-
|
|
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 };
|
package/lib/commands/check.js
CHANGED
|
@@ -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
|
|
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)
|
|
43
|
-
function fail(label, detail)
|
|
44
|
-
function warn(label, detail)
|
|
45
|
-
function info(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
|
-
|
|
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
|
-
|
|
122
|
-
|
|
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
|
-
|
|
128
|
+
apnsLines.push(kleur.cyan(`https://console.firebase.google.com/project/${firebaseProjectId}/settings/cloudmessaging`));
|
|
125
129
|
}
|
|
126
|
-
|
|
130
|
+
ui.note(apnsLines.join('\n'));
|
|
131
|
+
ui.outro('Done');
|
|
127
132
|
return;
|
|
128
133
|
}
|
|
129
134
|
|
|
130
135
|
if (!isSupabase) {
|
|
131
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
196
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
?
|
|
223
|
-
:
|
|
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
|
-
|
|
226
|
-
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
251
|
+
apnsLines.push(kleur.cyan(`https://console.firebase.google.com/project/${firebaseProjectId}/settings/cloudmessaging`));
|
|
236
252
|
}
|
|
237
|
-
|
|
253
|
+
ui.note(apnsLines.join('\n'));
|
|
254
|
+
ui.outro('Done');
|
|
238
255
|
}
|
|
239
256
|
|
|
240
257
|
module.exports = { runCheck };
|