kasy-cli 1.5.3 → 1.7.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 +7 -4
- package/docs/cli-reference.md +4 -6
- package/lib/commands/add.js +128 -102
- 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 +42 -20
- 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/scaffold/catalog.js +45 -11
- package/lib/scaffold/features/README.md +1 -2
- package/lib/scaffold/shared/generator-utils.js +1 -1
- 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/i18n.js +102 -78
- package/lib/utils/prompts.js +29 -177
- package/lib/utils/ui.js +92 -0
- package/lib/utils/updates.js +9 -8
- package/package.json +2 -1
- package/templates/firebase/lib/features/home/home_page.dart +0 -19
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_routes.dart +0 -1
- package/templates/firebase/lib/router.dart +0 -9
- package/templates/firebase/lib/features/dev/keyboard_test_page.dart +0 -93
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 {
|
|
@@ -276,7 +277,7 @@ function buildProgram(language) {
|
|
|
276
277
|
applyLocalizedHelp(
|
|
277
278
|
program
|
|
278
279
|
.command('add')
|
|
279
|
-
.argument('[module]', 'Module to add (e.g. sentry, analytics, revenuecat, onboarding,
|
|
280
|
+
.argument('[module]', 'Module to add (e.g. sentry, analytics, revenuecat, onboarding, llm_chat, ci...)')
|
|
280
281
|
.option('--list', 'List all available modules and their current status')
|
|
281
282
|
.option('--yes', 'Skip interactive prompts (use placeholder values)')
|
|
282
283
|
.option('-d, --directory <path>', 'Project folder (default: current directory)', '.')
|
|
@@ -290,7 +291,7 @@ function buildProgram(language) {
|
|
|
290
291
|
applyLocalizedHelp(
|
|
291
292
|
program
|
|
292
293
|
.command('remove')
|
|
293
|
-
.argument('[module]', 'Module to remove (e.g. sentry, analytics,
|
|
294
|
+
.argument('[module]', 'Module to remove (e.g. sentry, analytics, llm_chat, ci...)')
|
|
294
295
|
.option('--yes', 'Skip confirmation prompt')
|
|
295
296
|
.option('-d, --directory <path>', 'Project folder (default: current directory)', '.')
|
|
296
297
|
.description(t('cli.command.remove.description'))
|
|
@@ -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/docs/cli-reference.md
CHANGED
|
@@ -17,7 +17,7 @@ kasy new # interativo — pergunta tudo
|
|
|
17
17
|
kasy new meu-app # cria na pasta meu-app
|
|
18
18
|
kasy new --yes # modo rápido, preset Starter
|
|
19
19
|
kasy new --backend supabase # define backend sem perguntar
|
|
20
|
-
kasy new --with
|
|
20
|
+
kasy new --with llm_chat,sentry # pré-seleciona features
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
**Opções:**
|
|
@@ -36,12 +36,12 @@ kasy new --with camera,sentry # pré-seleciona features
|
|
|
36
36
|
Adiciona uma feature a um projeto Kasy existente.
|
|
37
37
|
|
|
38
38
|
```bash
|
|
39
|
-
kasy add
|
|
39
|
+
kasy add llm_chat
|
|
40
40
|
kasy add sentry
|
|
41
41
|
kasy add revenuecat
|
|
42
42
|
kasy add --list # lista features disponíveis e status
|
|
43
|
-
kasy add --yes
|
|
44
|
-
kasy add -d ./outro-app
|
|
43
|
+
kasy add --yes llm_chat # sem perguntas interativas
|
|
44
|
+
kasy add -d ./outro-app llm_chat
|
|
45
45
|
```
|
|
46
46
|
|
|
47
47
|
**Features disponíveis:**
|
|
@@ -54,7 +54,6 @@ kasy add -d ./outro-app camera
|
|
|
54
54
|
| `onboarding` | Fluxo de onboarding customizável |
|
|
55
55
|
| `web` | Suporte a Flutter Web |
|
|
56
56
|
| `widget` | Home widgets para iOS e Android |
|
|
57
|
-
| `camera` | Câmera com CameraAwesome |
|
|
58
57
|
| `llm_chat` | Chat com IA via Cloud/Edge Functions |
|
|
59
58
|
| `feedback` | Sistema de feedback e feature requests |
|
|
60
59
|
| `ci` | CI/CD com GitHub Actions / Codemagic |
|
|
@@ -202,7 +201,6 @@ lib/
|
|
|
202
201
|
home/ # tela principal
|
|
203
202
|
notifications/ # notificações locais e push
|
|
204
203
|
settings/ # configurações do app
|
|
205
|
-
camera/ # [feature] câmera
|
|
206
204
|
llm_chat/ # [feature] chat com IA
|
|
207
205
|
feedbacks/ # [feature] feedback e feature requests
|
|
208
206
|
onboarding/ # [feature] onboarding
|
package/lib/commands/add.js
CHANGED
|
@@ -5,23 +5,29 @@ 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,
|
|
13
13
|
FEATURES_PATCH_DIR,
|
|
14
14
|
normalizeFeature,
|
|
15
15
|
getVisibleFeatures,
|
|
16
|
+
getBaseFeatures,
|
|
17
|
+
BASE_FEATURES,
|
|
16
18
|
} = require('../scaffold/catalog');
|
|
17
19
|
|
|
20
|
+
function findBaseDisplayName(id) {
|
|
21
|
+
const base = BASE_FEATURES.find((f) => f.id === id);
|
|
22
|
+
return base ? base.displayName : id;
|
|
23
|
+
}
|
|
24
|
+
|
|
18
25
|
const KASY_AUDIENCE = process.env.KASY_INTERNAL === '1' ? 'internal' : 'public';
|
|
19
26
|
const { applyPatch } = require('../scaffold/engine');
|
|
20
27
|
const { writeRouter, writeNoOpAnalyticsApi, writeNoOpTrackingApi, writeNoOpAdminHomeWidgets, writeNoOpFeatureRequestRepository, writeMainDart, addPubspecDep, localizeReleaseDocs } = require('../scaffold/shared/generator-utils');
|
|
21
28
|
const { toPackageName, buildTokens } = require('../scaffold/backends/firebase/tokens');
|
|
22
29
|
|
|
23
30
|
const execAsync = promisify(exec);
|
|
24
|
-
const ora = oraPackage.default || oraPackage;
|
|
25
31
|
|
|
26
32
|
/** URL do endpoint LLM no app (espelha defineUpdates de llm_chat). */
|
|
27
33
|
function resolveLlmChatEndpoint(answers, kitSetup) {
|
|
@@ -317,58 +323,48 @@ const MODULE_META = {
|
|
|
317
323
|
},
|
|
318
324
|
};
|
|
319
325
|
|
|
326
|
+
/**
|
|
327
|
+
* Each entry returns a fn (t, cancel) -> Promise<value> that asks the user.
|
|
328
|
+
* Secret-like values (DSNs, API keys, tokens) use ui.password to mask input.
|
|
329
|
+
*/
|
|
320
330
|
const PROMPT_QUESTIONS = {
|
|
321
|
-
sentryDsn: (t) => ({
|
|
322
|
-
type: 'text',
|
|
323
|
-
name: 'sentryDsn',
|
|
331
|
+
sentryDsn: (t, cancel) => ui.password({
|
|
324
332
|
message: t('add.prompt.sentryDsn'),
|
|
325
|
-
|
|
333
|
+
onCancel: cancel,
|
|
326
334
|
}),
|
|
327
|
-
mixpanelToken: (t) => ({
|
|
328
|
-
type: 'text',
|
|
329
|
-
name: 'mixpanelToken',
|
|
335
|
+
mixpanelToken: (t, cancel) => ui.password({
|
|
330
336
|
message: t('add.prompt.mixpanelToken'),
|
|
331
|
-
|
|
337
|
+
onCancel: cancel,
|
|
332
338
|
}),
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
name: 'rcAndroidKey',
|
|
339
|
+
// RevenueCat SDK keys ship inside the client binary — not real secrets, plain text input.
|
|
340
|
+
rcAndroidKey: (t, cancel) => ui.text({
|
|
336
341
|
message: t('add.prompt.rcAndroidKey'),
|
|
337
|
-
|
|
342
|
+
onCancel: cancel,
|
|
338
343
|
}),
|
|
339
|
-
rcIosKey: (t) => ({
|
|
340
|
-
type: 'text',
|
|
341
|
-
name: 'rcIosKey',
|
|
344
|
+
rcIosKey: (t, cancel) => ui.text({
|
|
342
345
|
message: t('add.prompt.rcIosKey'),
|
|
343
|
-
|
|
346
|
+
onCancel: cancel,
|
|
344
347
|
}),
|
|
345
|
-
llmProvider: (t) => ({
|
|
346
|
-
type: 'select',
|
|
347
|
-
name: 'llmProvider',
|
|
348
|
+
llmProvider: (t, cancel) => ui.select({
|
|
348
349
|
message: t('add.prompt.llmProvider'),
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
{
|
|
350
|
+
initialValue: 'openai',
|
|
351
|
+
options: [
|
|
352
|
+
{ value: 'openai', label: 'OpenAI (gpt-4o-mini)' },
|
|
353
|
+
{ value: 'gemini', label: 'Google Gemini (gemini-1.5-flash)' },
|
|
352
354
|
],
|
|
353
|
-
|
|
355
|
+
onCancel: cancel,
|
|
354
356
|
}),
|
|
355
|
-
llmSystemPrompt: (t) => ({
|
|
356
|
-
type: 'text',
|
|
357
|
-
name: 'llmSystemPrompt',
|
|
357
|
+
llmSystemPrompt: (t, cancel) => ui.text({
|
|
358
358
|
message: t('add.prompt.llmSystemPrompt'),
|
|
359
|
-
|
|
359
|
+
onCancel: cancel,
|
|
360
360
|
}),
|
|
361
|
-
llmApiKey: (t) => ({
|
|
362
|
-
type: 'text',
|
|
363
|
-
name: 'llmApiKey',
|
|
361
|
+
llmApiKey: (t, cancel) => ui.password({
|
|
364
362
|
message: t('add.prompt.llmApiKey'),
|
|
365
|
-
|
|
363
|
+
onCancel: cancel,
|
|
366
364
|
}),
|
|
367
|
-
llmEndpoint: (t) => ({
|
|
368
|
-
type: 'text',
|
|
369
|
-
name: 'llmEndpoint',
|
|
365
|
+
llmEndpoint: (t, cancel) => ui.text({
|
|
370
366
|
message: t('add.prompt.llmEndpoint'),
|
|
371
|
-
|
|
367
|
+
onCancel: cancel,
|
|
372
368
|
}),
|
|
373
369
|
};
|
|
374
370
|
|
|
@@ -411,7 +407,7 @@ async function postAddLlmChat(projectDir, kitSetup, answers, t) {
|
|
|
411
407
|
|
|
412
408
|
// 2. Set API key as secret (skip if blank)
|
|
413
409
|
if (!apiKey) {
|
|
414
|
-
|
|
410
|
+
ui.log.warn(t('add.llm_chat.skipSecret'));
|
|
415
411
|
return { deployOk: false, deployAttempted: false };
|
|
416
412
|
}
|
|
417
413
|
|
|
@@ -419,21 +415,23 @@ async function postAddLlmChat(projectDir, kitSetup, answers, t) {
|
|
|
419
415
|
const refFlag = backend === 'supabase' && projectRef ? ` --project-ref ${projectRef}` : '';
|
|
420
416
|
|
|
421
417
|
if (backend === 'firebase') {
|
|
422
|
-
const spinner =
|
|
418
|
+
const spinner = ui.spinner();
|
|
419
|
+
spinner.start(t('add.llm_chat.settingSecret'));
|
|
423
420
|
try {
|
|
424
421
|
// Write to temp file — avoids trailing newline (echo) and shell injection risks
|
|
425
422
|
const tmpFile = path.join(os.tmpdir(), `llm_api_key_${Date.now()}.tmp`);
|
|
426
423
|
await fs.outputFile(tmpFile, apiKey, 'utf8');
|
|
427
424
|
await execAsync(`firebase functions:secrets:set LLM_API_KEY --data-file="${tmpFile}"`, { cwd: projectDir });
|
|
428
425
|
await fs.remove(tmpFile);
|
|
429
|
-
spinner.
|
|
426
|
+
spinner.stop(t('add.llm_chat.secretSet'));
|
|
430
427
|
secretsOk = true;
|
|
431
428
|
} catch {
|
|
432
|
-
spinner.
|
|
433
|
-
|
|
429
|
+
spinner.stop(`⚠ ${t('add.llm_chat.secretFailed')}`);
|
|
430
|
+
ui.log.message(kleur.dim('Run manually: firebase functions:secrets:set LLM_API_KEY'));
|
|
434
431
|
}
|
|
435
432
|
} else if (backend === 'supabase') {
|
|
436
|
-
const spinner =
|
|
433
|
+
const spinner = ui.spinner();
|
|
434
|
+
spinner.start(t('add.llm_chat.settingSecret'));
|
|
437
435
|
// Set LLM_API_KEY, LLM_PROVIDER and LLM_SYSTEM_PROMPT all as Supabase Secrets.
|
|
438
436
|
// Deployed Edge Functions read from Deno.env.get() = Supabase Secrets, NOT from .env files.
|
|
439
437
|
const esc = (v) => String(v).replace(/"/g, '\\"');
|
|
@@ -451,32 +449,33 @@ async function postAddLlmChat(projectDir, kitSetup, answers, t) {
|
|
|
451
449
|
if (r && r.ok === false) allOk = false;
|
|
452
450
|
}
|
|
453
451
|
if (allOk) {
|
|
454
|
-
spinner.
|
|
452
|
+
spinner.stop(t('add.llm_chat.secretSet'));
|
|
455
453
|
secretsOk = true;
|
|
456
454
|
} else {
|
|
457
|
-
spinner.
|
|
458
|
-
|
|
455
|
+
spinner.stop(`⚠ ${t('add.llm_chat.secretFailed')}`);
|
|
456
|
+
ui.log.message(kleur.dim(`Run manually: supabase secrets set LLM_API_KEY=YOUR_KEY LLM_PROVIDER=${provider}${refFlag}`));
|
|
459
457
|
}
|
|
460
458
|
}
|
|
461
459
|
|
|
462
460
|
// 3. Deploy the LLM function automatically
|
|
463
461
|
if (backend === 'api') return { deployOk: false, deployAttempted: false };
|
|
464
462
|
|
|
465
|
-
const deploySpinner =
|
|
463
|
+
const deploySpinner = ui.spinner();
|
|
464
|
+
deploySpinner.start(t('add.llm_chat.deploying'));
|
|
466
465
|
try {
|
|
467
466
|
if (backend === 'firebase') {
|
|
468
467
|
await execAsync('firebase deploy --only functions:llmChat', { cwd: projectDir, timeout: 180_000 });
|
|
469
468
|
} else if (backend === 'supabase') {
|
|
470
469
|
await execAsync(`supabase functions deploy llm-chat --no-verify-jwt${refFlag}`, { cwd: projectDir, timeout: 180_000 });
|
|
471
470
|
}
|
|
472
|
-
deploySpinner.
|
|
471
|
+
deploySpinner.stop(t('add.llm_chat.deployed'));
|
|
473
472
|
return { deployOk: true, deployAttempted: true };
|
|
474
473
|
} catch {
|
|
475
|
-
deploySpinner.
|
|
474
|
+
deploySpinner.stop(`⚠ ${t('add.llm_chat.deployFailed')}`);
|
|
476
475
|
if (backend === 'firebase') {
|
|
477
|
-
|
|
476
|
+
ui.log.message(kleur.dim('Run manually: firebase deploy --only functions:llmChat'));
|
|
478
477
|
} else {
|
|
479
|
-
|
|
478
|
+
ui.log.message(kleur.dim(`Run manually: supabase functions deploy llm-chat --no-verify-jwt${refFlag}`));
|
|
480
479
|
}
|
|
481
480
|
return { deployOk: false, deployAttempted: true };
|
|
482
481
|
}
|
|
@@ -485,8 +484,6 @@ async function postAddLlmChat(projectDir, kitSetup, answers, t) {
|
|
|
485
484
|
// ── List command ──────────────────────────────────────────────────────────────
|
|
486
485
|
|
|
487
486
|
async function listModules(projectDir, t) {
|
|
488
|
-
console.log(kleur.bold(`\n${t('add.list.title')}\n`));
|
|
489
|
-
|
|
490
487
|
let activeModules = [];
|
|
491
488
|
const kitSetupPath = path.join(projectDir, 'kit_setup.json');
|
|
492
489
|
if (await fs.pathExists(kitSetupPath)) {
|
|
@@ -498,14 +495,33 @@ async function listModules(projectDir, t) {
|
|
|
498
495
|
}
|
|
499
496
|
}
|
|
500
497
|
|
|
498
|
+
const lines = [];
|
|
499
|
+
|
|
500
|
+
lines.push(kleur.bold(t('modules.featuresBase')));
|
|
501
|
+
for (const feature of getBaseFeatures()) {
|
|
502
|
+
lines.push(`${kleur.green('✓')} ${kleur.green(feature.displayName)}`);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Subscriptions is base, but inactive until RevenueCat is added.
|
|
506
|
+
if (!activeModules.includes('revenuecat')) {
|
|
507
|
+
lines.push('');
|
|
508
|
+
lines.push(kleur.dim(t('modules.hint.subscriptionNoRc')));
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
lines.push('');
|
|
512
|
+
lines.push(kleur.bold(t('modules.features')));
|
|
501
513
|
for (const feature of getVisibleFeatures({ audience: KASY_AUDIENCE })) {
|
|
502
514
|
const active = activeModules.includes(feature.id);
|
|
503
|
-
const
|
|
515
|
+
const betaBadge = feature.status === 'internal' ? kleur.yellow(' [beta]') : '';
|
|
516
|
+
const tagBadge = feature.tag ? kleur.yellow(` [${t(`modules.tag.${feature.tag}`)}]`) : '';
|
|
517
|
+
const enhancesBadge = feature.enhances
|
|
518
|
+
? kleur.magenta(` [${t('modules.tag.enhances', { target: findBaseDisplayName(feature.enhances) })}]`)
|
|
519
|
+
: '';
|
|
504
520
|
const icon = active ? kleur.green('✓') : kleur.dim('○');
|
|
505
|
-
const label = active ? kleur.green(feature.
|
|
506
|
-
|
|
521
|
+
const label = active ? kleur.green(feature.displayName) : kleur.white(feature.displayName);
|
|
522
|
+
lines.push(`${icon} ${label}${tagBadge}${enhancesBadge}${betaBadge}`);
|
|
507
523
|
}
|
|
508
|
-
|
|
524
|
+
ui.note(lines.join('\n'), t('add.list.title'));
|
|
509
525
|
}
|
|
510
526
|
|
|
511
527
|
// ── Main command ──────────────────────────────────────────────────────────────
|
|
@@ -513,16 +529,23 @@ async function listModules(projectDir, t) {
|
|
|
513
529
|
async function runAdd(module, options = {}) {
|
|
514
530
|
const t = createTranslator(options.language || detectDefaultLanguage());
|
|
515
531
|
const projectDir = path.resolve(options.directory || '.');
|
|
532
|
+
const cancel = () => { ui.cancel(t('add.cancelled')); process.exit(0); };
|
|
516
533
|
|
|
517
534
|
// --list flag
|
|
518
535
|
if (options.list) {
|
|
536
|
+
printCompactHeader(t);
|
|
537
|
+
ui.intro(t('add.list.title'));
|
|
519
538
|
await listModules(projectDir, t);
|
|
539
|
+
ui.outro('');
|
|
520
540
|
return;
|
|
521
541
|
}
|
|
522
542
|
|
|
523
543
|
if (!module) {
|
|
524
|
-
|
|
544
|
+
printCompactHeader(t);
|
|
545
|
+
ui.intro(t('add.list.title'));
|
|
546
|
+
ui.log.warn(t('add.error.noModule'));
|
|
525
547
|
await listModules(projectDir, t);
|
|
548
|
+
ui.outro('');
|
|
526
549
|
return;
|
|
527
550
|
}
|
|
528
551
|
|
|
@@ -542,13 +565,16 @@ async function runAdd(module, options = {}) {
|
|
|
542
565
|
if (moduleLower === 'ios-release' || moduleLower === 'ios_release' || moduleLower === 'iosrelease') {
|
|
543
566
|
const scriptPath = path.join(projectDir, 'scripts', 'release-ios.sh');
|
|
544
567
|
if (await fs.pathExists(scriptPath)) {
|
|
545
|
-
|
|
568
|
+
printCompactHeader(t);
|
|
569
|
+
ui.log.warn(t('add.iosRelease.already'));
|
|
546
570
|
return;
|
|
547
571
|
}
|
|
548
572
|
const patchDir = path.join(FEATURES_PATCH_DIR, 'ios-release');
|
|
549
573
|
if (!(await fs.pathExists(patchDir))) {
|
|
550
574
|
throw new Error(t('add.error.unknownModule', { module, list: 'ios-release' }));
|
|
551
575
|
}
|
|
576
|
+
printCompactHeader(t);
|
|
577
|
+
ui.intro(t('add.applying', { module: 'ios-release' }));
|
|
552
578
|
const { tokens, pathReplacements } = buildTokens({
|
|
553
579
|
appName: kitSetup.appName,
|
|
554
580
|
bundleId: kitSetup.bundleId,
|
|
@@ -557,8 +583,8 @@ async function runAdd(module, options = {}) {
|
|
|
557
583
|
await localizeReleaseDocs(projectDir, options.language || detectDefaultLanguage());
|
|
558
584
|
await ensureIosReleaseMakefile(projectDir);
|
|
559
585
|
await ensureGitignoreKasyEnv(projectDir);
|
|
560
|
-
|
|
561
|
-
|
|
586
|
+
ui.log.success(t('add.iosRelease.success'));
|
|
587
|
+
ui.outro(kleur.cyan('kasy ios configure'));
|
|
562
588
|
return;
|
|
563
589
|
}
|
|
564
590
|
|
|
@@ -578,23 +604,22 @@ async function runAdd(module, options = {}) {
|
|
|
578
604
|
if (activeModules.includes(normalized)) {
|
|
579
605
|
// llm_chat can be re-run to reconfigure credentials even when already active
|
|
580
606
|
if (normalized !== 'llm_chat') {
|
|
581
|
-
|
|
607
|
+
printCompactHeader(t);
|
|
608
|
+
ui.log.warn(t('add.alreadyActive', { module: normalized }));
|
|
582
609
|
return;
|
|
583
610
|
}
|
|
584
|
-
|
|
611
|
+
printCompactHeader(t);
|
|
612
|
+
ui.intro(t('add.llm_chat.reconfigure'));
|
|
585
613
|
// Skip to credential-only flow: prompt → postAdd → done
|
|
586
614
|
const answers = {};
|
|
587
615
|
if (!options.yes) {
|
|
588
|
-
console.log('');
|
|
589
616
|
for (const key of ['llmProvider', 'llmSystemPrompt', 'llmApiKey']) {
|
|
590
|
-
const
|
|
591
|
-
if (!
|
|
592
|
-
|
|
593
|
-
answers[key] = response[key] ?? '';
|
|
617
|
+
const ask = PROMPT_QUESTIONS[key];
|
|
618
|
+
if (!ask) continue;
|
|
619
|
+
answers[key] = (await ask(t, cancel)) ?? '';
|
|
594
620
|
}
|
|
595
621
|
if (kitSetup.backendProvider === 'api') {
|
|
596
|
-
|
|
597
|
-
answers.llmEndpoint = response.llmEndpoint || '';
|
|
622
|
+
answers.llmEndpoint = (await PROMPT_QUESTIONS.llmEndpoint(t, cancel)) || '';
|
|
598
623
|
}
|
|
599
624
|
}
|
|
600
625
|
const defines = MODULE_META.llm_chat.defineUpdates(answers, kitSetup);
|
|
@@ -610,41 +635,37 @@ async function runAdd(module, options = {}) {
|
|
|
610
635
|
const { deployOk, deployAttempted } = await postAddLlmChat(projectDir, kitSetup, answers, t);
|
|
611
636
|
const backend = kitSetup?.backendProvider ?? 'firebase';
|
|
612
637
|
const needsManualDeploy = deployAttempted && !deployOk;
|
|
638
|
+
let nextStepsMsg;
|
|
613
639
|
if (backend === 'firebase') {
|
|
614
|
-
|
|
640
|
+
nextStepsMsg = t(needsManualDeploy ? 'add.llm_chat.nextSteps.firebase.deployFailed' : 'add.llm_chat.nextSteps.firebase');
|
|
615
641
|
} else if (backend === 'supabase') {
|
|
616
|
-
|
|
642
|
+
nextStepsMsg = t(needsManualDeploy ? 'add.llm_chat.nextSteps.supabase.deployFailed' : 'add.llm_chat.nextSteps.supabase');
|
|
617
643
|
} else {
|
|
618
|
-
|
|
644
|
+
nextStepsMsg = t('add.llm_chat.nextSteps.api');
|
|
619
645
|
}
|
|
646
|
+
ui.outro(kleur.cyan(nextStepsMsg));
|
|
620
647
|
return;
|
|
621
648
|
}
|
|
622
649
|
|
|
623
650
|
const meta = MODULE_META[normalized] || { promptKeys: [], defineUpdates: () => ({}), envLines: () => [], featureFlag: null };
|
|
624
651
|
|
|
652
|
+
printCompactHeader(t);
|
|
653
|
+
ui.intro(t('add.applying', { module: normalized }));
|
|
654
|
+
|
|
625
655
|
// 4. Prompt for API keys (skip if --yes)
|
|
626
656
|
const answers = {};
|
|
627
657
|
if (!options.yes && meta.promptKeys.length > 0) {
|
|
628
|
-
console.log('');
|
|
629
658
|
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] ?? '';
|
|
659
|
+
const ask = PROMPT_QUESTIONS[key];
|
|
660
|
+
if (!ask) continue;
|
|
661
|
+
answers[key] = (await ask(t, cancel)) ?? '';
|
|
636
662
|
}
|
|
637
663
|
// Extra prompt for API backend (custom LLM endpoint)
|
|
638
664
|
if (normalized === 'llm_chat' && kitSetup.backendProvider === 'api') {
|
|
639
|
-
|
|
640
|
-
onCancel: () => { throw new Error(t('add.cancelled')); },
|
|
641
|
-
});
|
|
642
|
-
answers.llmEndpoint = response.llmEndpoint || '';
|
|
665
|
+
answers.llmEndpoint = (await PROMPT_QUESTIONS.llmEndpoint(t, cancel)) || '';
|
|
643
666
|
}
|
|
644
667
|
}
|
|
645
668
|
|
|
646
|
-
console.log(kleur.bold(`\n${t('add.applying', { module: normalized })}\n`));
|
|
647
|
-
|
|
648
669
|
// 5. Enable feature flag in features.dart
|
|
649
670
|
if (meta.featureFlag) {
|
|
650
671
|
await enableFeatureFlag(projectDir, meta.featureFlag);
|
|
@@ -667,16 +688,17 @@ async function runAdd(module, options = {}) {
|
|
|
667
688
|
// 8. Apply patch if it exists under features/<module>/
|
|
668
689
|
const patchDir = path.join(FEATURES_PATCH_DIR, normalized);
|
|
669
690
|
if (await fs.pathExists(patchDir)) {
|
|
670
|
-
const spinner =
|
|
691
|
+
const spinner = ui.spinner();
|
|
692
|
+
spinner.start(t('add.applyingPatch'));
|
|
671
693
|
try {
|
|
672
694
|
const { tokens: patchTokens, pathReplacements: patchPathReplacements } = buildTokens({
|
|
673
695
|
appName: kitSetup.appName,
|
|
674
696
|
bundleId: kitSetup.bundleId,
|
|
675
697
|
});
|
|
676
698
|
await applyPatch(patchDir, projectDir, patchTokens, patchPathReplacements);
|
|
677
|
-
spinner.
|
|
699
|
+
spinner.stop(t('add.patchApplied'));
|
|
678
700
|
} catch (err) {
|
|
679
|
-
spinner.
|
|
701
|
+
spinner.error(t('add.patchFailed'));
|
|
680
702
|
throw err;
|
|
681
703
|
}
|
|
682
704
|
}
|
|
@@ -726,24 +748,26 @@ async function runAdd(module, options = {}) {
|
|
|
726
748
|
|
|
727
749
|
// 10. flutter pub get
|
|
728
750
|
{
|
|
729
|
-
const spinner =
|
|
751
|
+
const spinner = ui.spinner();
|
|
752
|
+
spinner.start(t('add.pubGet'));
|
|
730
753
|
try {
|
|
731
754
|
await execAsync('flutter pub get', { cwd: projectDir, timeout: 300_000 });
|
|
732
|
-
spinner.
|
|
755
|
+
spinner.stop(t('add.pubGetDone'));
|
|
733
756
|
} catch {
|
|
734
|
-
spinner.
|
|
757
|
+
spinner.stop(`⚠ ${t('add.pubGetFailed')}`);
|
|
735
758
|
}
|
|
736
759
|
}
|
|
737
760
|
|
|
738
761
|
// 11. build_runner (only when needed: features with codegen)
|
|
739
762
|
const needsBuildRunner = ['revenuecat', 'analytics', 'sentry', 'onboarding', 'llm_chat', 'feedback'].includes(normalized);
|
|
740
763
|
if (needsBuildRunner) {
|
|
741
|
-
const spinner =
|
|
764
|
+
const spinner = ui.spinner();
|
|
765
|
+
spinner.start(t('add.buildRunner'));
|
|
742
766
|
try {
|
|
743
767
|
await execAsync('dart run build_runner build --delete-conflicting-outputs', { cwd: projectDir, timeout: 600_000 });
|
|
744
|
-
spinner.
|
|
768
|
+
spinner.stop(t('add.buildRunnerDone'));
|
|
745
769
|
} catch {
|
|
746
|
-
spinner.
|
|
770
|
+
spinner.stop(`⚠ ${t('add.buildRunnerFailed')}`);
|
|
747
771
|
}
|
|
748
772
|
}
|
|
749
773
|
|
|
@@ -755,23 +779,25 @@ async function runAdd(module, options = {}) {
|
|
|
755
779
|
|
|
756
780
|
// Show note if any
|
|
757
781
|
if (meta.note) {
|
|
758
|
-
|
|
782
|
+
ui.log.message(kleur.dim(t(meta.note)));
|
|
759
783
|
}
|
|
760
784
|
|
|
761
|
-
console.log(kleur.green(`\n✓ ${t('add.success', { module: normalized })}\n`));
|
|
762
|
-
|
|
763
785
|
// LLM Chat: show next steps (adjust based on whether deploy succeeded)
|
|
764
786
|
if (normalized === 'llm_chat') {
|
|
765
787
|
const backend = kitSetup?.backendProvider ?? 'firebase';
|
|
766
788
|
const needsManualDeploy = llmDeployResult?.deployAttempted && !llmDeployResult?.deployOk;
|
|
789
|
+
let nextStepsMsg;
|
|
767
790
|
if (backend === 'firebase') {
|
|
768
|
-
|
|
791
|
+
nextStepsMsg = t(needsManualDeploy ? 'add.llm_chat.nextSteps.firebase.deployFailed' : 'add.llm_chat.nextSteps.firebase');
|
|
769
792
|
} else if (backend === 'supabase') {
|
|
770
|
-
|
|
793
|
+
nextStepsMsg = t(needsManualDeploy ? 'add.llm_chat.nextSteps.supabase.deployFailed' : 'add.llm_chat.nextSteps.supabase');
|
|
771
794
|
} else {
|
|
772
|
-
|
|
795
|
+
nextStepsMsg = t('add.llm_chat.nextSteps.api');
|
|
773
796
|
}
|
|
797
|
+
ui.log.info(nextStepsMsg);
|
|
774
798
|
}
|
|
799
|
+
|
|
800
|
+
ui.outro(t('add.success', { module: normalized }));
|
|
775
801
|
}
|
|
776
802
|
|
|
777
803
|
module.exports = { runAdd };
|