kasy-cli 1.9.2 → 1.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/kasy.js CHANGED
@@ -242,14 +242,15 @@ function buildProgram(language) {
242
242
  const langName = t('cli.command.setup.langName');
243
243
  applyLocalizedHelp(
244
244
  program
245
- .command('setup')
245
+ .command('setup', { hidden: true })
246
246
  .argument(`[${directoryName}]`, t('cli.command.setup.directoryArg'), '.')
247
247
  .option(`-l, --lang <${langName}>`, t('cli.command.setup.langOption'))
248
248
  .option('-b, --backend <backend>', t('cli.command.setup.backendOption'))
249
249
  .option('--with <features>', t('cli.command.setup.featuresOption'))
250
250
  .description(t('cli.command.setup.description'))
251
251
  .action(async (directory, options) => {
252
- // `setup` is an alias for `new` — unified flow
252
+ // `setup` is an alias for `new` — unified flow.
253
+ // Hidden from root help to reduce noise; still callable.
253
254
  await runNew(directory, {
254
255
  language: options.lang,
255
256
  backend: options.backend,
@@ -516,7 +517,6 @@ function buildProgram(language) {
516
517
  notificationsCmd
517
518
  .command('text')
518
519
  .argument('[directory]', 'Project folder (default: current directory)', '.')
519
- .option('-d, --directory <path>', 'Project folder (default: current directory)')
520
520
  .option('--locale <code>', 'i18n files to update: pt, en, es, or all (default: all)', 'all')
521
521
  .option('--demo-title <text>', 'Home → Features demo notification title')
522
522
  .option('--demo-body <text>', 'Home → Features demo card description / instant notification body')
@@ -524,7 +524,7 @@ function buildProgram(language) {
524
524
  .option('--reminder-body <text>', 'Scheduled reminder notification body')
525
525
  .description(t('cli.command.notifications.text.description'))
526
526
  .action(async (directory, options) => {
527
- const dir = options.directory || directory || '.';
527
+ const dir = directory || '.';
528
528
  await runNotificationsText(dir, {
529
529
  language,
530
530
  directory: dir,
@@ -110,67 +110,67 @@ async function runCheck(options = {}) {
110
110
  const t = createTranslator(lang);
111
111
 
112
112
  printCompactHeader(t);
113
- ui.intro('Kasy Check — Push Notifications');
113
+ ui.intro(t('check.intro'));
114
114
 
115
115
  // ── Detect backend ────────────────────────────────────────────────────────
116
116
  const isFirebase = await fs.pathExists(path.join(projectDir, 'firebase.json'));
117
117
  const isSupabase = await fs.pathExists(path.join(projectDir, 'supabase'));
118
118
 
119
119
  if (isFirebase && !isSupabase) {
120
- ok('Firebase backend');
121
- info('Firebase usa Application Default Credentials — nenhuma configuração extra necessária.');
120
+ ok(t('check.firebase.detected'));
121
+ info(t('check.firebase.adcInfo'));
122
122
  const firebaseProjectId = await readFirebaseProjectId(projectDir);
123
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'),
124
+ kleur.yellow(t('check.apns.warn')),
125
+ kleur.dim(t('check.apns.where')),
126
126
  ];
127
127
  if (firebaseProjectId) {
128
128
  apnsLines.push(kleur.cyan(`https://console.firebase.google.com/project/${firebaseProjectId}/settings/cloudmessaging`));
129
129
  }
130
130
  ui.note(apnsLines.join('\n'));
131
- ui.outro('Done');
131
+ ui.outro(t('check.done'));
132
132
  return;
133
133
  }
134
134
 
135
135
  if (!isSupabase) {
136
- ui.log.error('Diretório não parece ser um projeto Kasy.');
137
- ui.cancel('Aborted');
136
+ ui.log.error(t('check.notKasy'));
137
+ ui.cancel(t('check.aborted'));
138
138
  process.exit(1);
139
139
  }
140
140
 
141
141
  // ── 1. Project linked? ────────────────────────────────────────────────────
142
142
  const projectRef = await readProjectRef(projectDir);
143
143
  if (!projectRef) {
144
- fail('Projeto Supabase não vinculado', 'execute: supabase link --project-ref SEU_REF');
145
- ui.cancel('Aborted');
144
+ fail(t('check.supabase.notLinked'), t('check.supabase.notLinkedHint'));
145
+ ui.cancel(t('check.aborted'));
146
146
  process.exit(1);
147
147
  }
148
- ok('Projeto vinculado', projectRef);
148
+ ok(t('check.supabase.linked'), projectRef);
149
149
 
150
150
  // ── Read secrets list ─────────────────────────────────────────────────────
151
151
  const spinner = ui.spinner();
152
- spinner.start('Verificando secrets');
152
+ spinner.start(t('check.spin.secrets'));
153
153
  const secrets = await listSecretNames(projectDir);
154
- spinner.stop('Secrets verificados');
154
+ spinner.stop(t('check.spin.secretsDone'));
155
155
 
156
156
  if (!secrets) {
157
- warn('Não foi possível listar secrets', 'verifique: supabase login');
157
+ warn(t('check.secrets.listFailed'), t('check.secrets.checkLogin'));
158
158
  }
159
159
 
160
160
  // ── 2. FIREBASE_PROJECT_ID ────────────────────────────────────────────────
161
161
  const firebaseProjectId = await readFirebaseProjectId(projectDir);
162
162
  if (secrets) {
163
163
  if (secrets.has('FIREBASE_PROJECT_ID')) {
164
- ok('FIREBASE_PROJECT_ID configurado');
164
+ ok(t('check.fbProjId.ok'));
165
165
  } else {
166
- fail('FIREBASE_PROJECT_ID ausente');
166
+ fail(t('check.fbProjId.missing'));
167
167
  if (firebaseProjectId) {
168
- ui.log.message(kleur.gray(`Corrija: supabase secrets set FIREBASE_PROJECT_ID="${firebaseProjectId}"`));
168
+ ui.log.message(kleur.gray(t('check.fbProjId.fixHint', { id: firebaseProjectId })));
169
169
  if (options.fix) {
170
170
  const r = await runCmd(`supabase secrets set FIREBASE_PROJECT_ID="${firebaseProjectId}"`, projectDir);
171
171
  r.ok
172
- ? ok('FIREBASE_PROJECT_ID → configurado automaticamente')
173
- : fail('FIREBASE_PROJECT_ID → falhou ao configurar', r.error);
172
+ ? ok(t('check.fbProjId.fixed'))
173
+ : fail(t('check.fbProjId.fixFailed'), r.error);
174
174
  }
175
175
  }
176
176
  }
@@ -179,15 +179,15 @@ async function runCheck(options = {}) {
179
179
  // ── 3. FIREBASE_SERVICE_ACCOUNT_JSON ─────────────────────────────────────
180
180
  if (secrets) {
181
181
  if (secrets.has('FIREBASE_SERVICE_ACCOUNT_JSON')) {
182
- ok('FIREBASE_SERVICE_ACCOUNT_JSON configurado');
182
+ ok(t('check.fbSak.ok'));
183
183
  } else {
184
- fail('FIREBASE_SERVICE_ACCOUNT_JSON ausente');
184
+ fail(t('check.fbSak.missing'));
185
185
 
186
186
  if (options.fix && firebaseProjectId) {
187
187
  const fixSpinner = ui.spinner();
188
- fixSpinner.start('Gerando e configurando chave FCM…');
188
+ fixSpinner.start(t('check.fbSak.spin'));
189
189
  const fcmResult = await createFcmServiceAccountKey(firebaseProjectId);
190
- fixSpinner.stop('Chave FCM gerada');
190
+ fixSpinner.stop(t('check.fbSak.spinDone'));
191
191
 
192
192
  if (fcmResult.ok) {
193
193
  const secretSteps = await setSupabaseSecrets(projectDir, {
@@ -196,62 +196,52 @@ async function runCheck(options = {}) {
196
196
  });
197
197
  const setStep = secretSteps.find((s) => s.name === 'secret FIREBASE_SERVICE_ACCOUNT_JSON');
198
198
  setStep?.ok
199
- ? ok('FIREBASE_SERVICE_ACCOUNT_JSON → configurado automaticamente')
200
- : fail('FIREBASE_SERVICE_ACCOUNT_JSON → falhou ao configurar', setStep?.error);
199
+ ? ok(t('check.fbSak.fixed'))
200
+ : fail(t('check.fbSak.fixFailed'), setStep?.error);
201
201
  } else {
202
- fail('FIREBASE_SERVICE_ACCOUNT_JSON → não foi possível gerar chave', fcmResult.error);
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
- ));
202
+ fail(t('check.fbSak.genFailed'), fcmResult.error);
203
+ ui.log.message(kleur.gray(t('check.fbSak.manual')));
207
204
  }
208
205
  } else {
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
- ));
206
+ ui.log.message(kleur.gray(t('check.fbSak.hint')));
214
207
  }
215
208
  }
216
209
  }
217
210
 
218
211
  // ── 4. Edge function send-push-notification ────────────────────────────────
219
212
  const fnSpinner = ui.spinner();
220
- fnSpinner.start('Verificando edge functions…');
213
+ fnSpinner.start(t('check.fn.spin'));
221
214
  const functions = await listDeployedFunctions(projectDir);
222
- fnSpinner.stop('Edge functions verificadas');
215
+ fnSpinner.stop(t('check.fn.spinDone'));
223
216
 
224
217
  if (!functions) {
225
- warn('Não foi possível listar edge functions', 'verifique: supabase login');
218
+ warn(t('check.fn.listFailed'), t('check.secrets.checkLogin'));
226
219
  } else if (functions.has('send-push-notification')) {
227
- ok('Edge function send-push-notification deployada');
220
+ ok(t('check.fn.deployed'));
228
221
  } else {
229
- fail('Edge function send-push-notification não deployada');
222
+ fail(t('check.fn.missing'));
230
223
  if (options.fix) {
231
224
  const deploySpinner = ui.spinner();
232
- deploySpinner.start('Publicando send-push-notification…');
225
+ deploySpinner.start(t('check.fn.deploySpin'));
233
226
  const r = await runCmd('supabase functions deploy send-push-notification', projectDir);
234
227
  r.ok
235
- ? deploySpinner.stop('send-push-notification → deployada automaticamente')
236
- : deploySpinner.error(`send-push-notification → falhou no deploy${r.error ? ` — ${r.error}` : ''}`);
228
+ ? deploySpinner.stop(t('check.fn.deployDone'))
229
+ : deploySpinner.error(`${t('check.fn.deployFailed')}${r.error ? ` — ${r.error}` : ''}`);
237
230
  } else {
238
- ui.log.message(kleur.gray(
239
- 'Corrija automaticamente: kasy check --fix\n' +
240
- 'Ou manualmente: supabase functions deploy send-push-notification'
241
- ));
231
+ ui.log.message(kleur.gray(t('check.fn.hint')));
242
232
  }
243
233
  }
244
234
 
245
235
  // ── 5. APNs reminder ─────────────────────────────────────────────────────
246
236
  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'),
237
+ kleur.yellow(t('check.apns.warn')),
238
+ kleur.dim(t('check.apns.where')),
249
239
  ];
250
240
  if (firebaseProjectId) {
251
241
  apnsLines.push(kleur.cyan(`https://console.firebase.google.com/project/${firebaseProjectId}/settings/cloudmessaging`));
252
242
  }
253
243
  ui.note(apnsLines.join('\n'));
254
- ui.outro('Done');
244
+ ui.outro(t('check.done'));
255
245
  }
256
246
 
257
247
  module.exports = { runCheck };
@@ -123,14 +123,14 @@ async function deployFirebase(projectDir, options, tr) {
123
123
  }
124
124
 
125
125
  const spinner = ui.spinner();
126
- spinner.start('Deploying Firebase...');
126
+ spinner.start(tr('deploy.firebase.spin'));
127
127
  let steps;
128
128
  try {
129
129
  steps = await runFirebaseDeploy(projectDir, null, firebaseProjectId, {
130
130
  functionsRegion,
131
131
  onProgress: (key) => { spinner.message(String(key)); },
132
132
  });
133
- spinner.stop('Firebase deploy completed');
133
+ spinner.stop(tr('deploy.firebase.spinDone'));
134
134
  } catch (err) {
135
135
  spinner.error(err.message);
136
136
  throw err;
@@ -145,9 +145,9 @@ async function deployFirebase(projectDir, options, tr) {
145
145
  // ── APNs reminder — required for iOS push notifications ──────────────────
146
146
  // Cannot be automated: the .p8 key only exists in Apple Developer Portal.
147
147
  const apnsBody = [
148
- kleur.yellow('Push iOS: configure a APNs Key no Firebase Console'),
149
- kleur.dim('1. Apple Developer Portal → Keys → criar APNs Key (.p8)'),
150
- kleur.dim('2. Firebase Console → Cloud Messaging → app iOS → upload da APNs Key'),
148
+ kleur.yellow(tr('deploy.apns.title')),
149
+ kleur.dim(tr('deploy.apns.step1')),
150
+ kleur.dim(tr('deploy.apns.step2')),
151
151
  kleur.cyan(`https://console.firebase.google.com/project/${firebaseProjectId}/settings/cloudmessaging`),
152
152
  ].join('\n');
153
153
  ui.note(apnsBody);
@@ -155,28 +155,28 @@ async function deployFirebase(projectDir, options, tr) {
155
155
 
156
156
  // ── Supabase deploy ───────────────────────────────────────────────────────────
157
157
 
158
- async function deploySupabase(projectDir) {
158
+ async function deploySupabase(projectDir, tr) {
159
159
  // ── 1. Get project ref ──────────────────────────────────────────────────
160
160
  const projectRef = await readSupabaseProjectRef(projectDir);
161
161
  if (!projectRef) {
162
- ui.log.error('Projeto Supabase não está vinculado neste diretório.');
163
- ui.log.message(kleur.gray('Execute: supabase link --project-ref SEU_PROJECT_REF'));
162
+ ui.log.error(tr('deploy.supabase.notLinked'));
163
+ ui.log.message(kleur.gray(tr('deploy.supabase.linkHint')));
164
164
  process.exit(1);
165
165
  }
166
- ui.log.message(kleur.gray(`Project ref: ${kleur.cyan(projectRef)}`));
166
+ ui.log.message(kleur.gray(tr('deploy.supabase.projectRef', { ref: kleur.cyan(projectRef) })));
167
167
 
168
168
  // ── 2. FCM Service Account JSON ─────────────────────────────────────────
169
169
  const alreadySet = await isServiceAccountJsonSet(projectDir);
170
170
  if (alreadySet) {
171
- printStep(true, false, 'FIREBASE_SERVICE_ACCOUNT_JSON já configurado');
171
+ printStep(true, false, tr('deploy.supabase.sakAlready'));
172
172
  } else {
173
173
  const firebaseProjectId = await readFirebaseProjectId(projectDir);
174
174
 
175
175
  if (firebaseProjectId) {
176
176
  const fcmSpinner = ui.spinner();
177
- fcmSpinner.start('Gerando chave FCM (Service Account)…');
177
+ fcmSpinner.start(tr('deploy.supabase.sakSpin'));
178
178
  const fcmResult = await createFcmServiceAccountKey(firebaseProjectId);
179
- fcmSpinner.stop('Chave FCM gerada');
179
+ fcmSpinner.stop(tr('deploy.supabase.sakSpinDone'));
180
180
 
181
181
  if (fcmResult.ok) {
182
182
  const secretSteps = await setSupabaseSecrets(projectDir, {
@@ -186,23 +186,23 @@ async function deploySupabase(projectDir) {
186
186
  for (const s of secretSteps) printStep(s.ok, false, s.name, s.error);
187
187
  } else {
188
188
  printStep(false, false, 'FIREBASE_SERVICE_ACCOUNT_JSON', fcmResult.error);
189
- ui.log.warn("Configure manualmente: supabase secrets set FIREBASE_SERVICE_ACCOUNT_JSON='...'");
189
+ ui.log.warn(tr('deploy.supabase.sakManual'));
190
190
  }
191
191
  } else {
192
- printStep(false, false, 'FIREBASE_SERVICE_ACCOUNT_JSON', 'google-services.json não encontrado — configure manualmente');
192
+ printStep(false, false, 'FIREBASE_SERVICE_ACCOUNT_JSON', tr('deploy.supabase.sakNoGS'));
193
193
  }
194
194
  }
195
195
 
196
196
  // ── 3. Deploy edge functions ────────────────────────────────────────────
197
197
  const fnSpinner = ui.spinner();
198
- fnSpinner.start('Publicando edge functions…');
198
+ fnSpinner.start(tr('deploy.supabase.fnSpin'));
199
199
  const fnResult = await deployFunctions(projectDir);
200
- fnSpinner.stop('Edge functions processadas');
200
+ fnSpinner.stop(tr('deploy.supabase.fnSpinDone'));
201
201
 
202
202
  if (Array.isArray(fnResult)) {
203
203
  fnResult.forEach((s) => printStep(s.ok, s.skipped, s.name, s.error));
204
204
  } else if (fnResult.skipped) {
205
- printStep(true, true, 'edge functions (nenhuma encontrada)');
205
+ printStep(true, true, tr('deploy.supabase.fnNone'));
206
206
  } else {
207
207
  printStep(fnResult.ok, false, 'supabase functions deploy', fnResult.error);
208
208
  }
@@ -210,9 +210,9 @@ async function deploySupabase(projectDir) {
210
210
  // ── 4. APNs reminder ────────────────────────────────────────────────────
211
211
  const firebaseProjectId = await readFirebaseProjectId(projectDir);
212
212
  const apnsLines = [
213
- kleur.yellow('Push iOS: configure a APNs Key no Firebase Console'),
214
- kleur.dim('1. Apple Developer Portal → Keys → criar APNs Key (.p8)'),
215
- kleur.dim('2. Firebase Console → Cloud Messaging → app iOS → upload da APNs Key'),
213
+ kleur.yellow(tr('deploy.apns.title')),
214
+ kleur.dim(tr('deploy.apns.step1')),
215
+ kleur.dim(tr('deploy.apns.step2')),
216
216
  ];
217
217
  if (firebaseProjectId) {
218
218
  apnsLines.push(kleur.cyan(`https://console.firebase.google.com/project/${firebaseProjectId}/settings/cloudmessaging`));
@@ -240,17 +240,17 @@ async function runDeployCommand(directory, options = {}, { language: langHint }
240
240
 
241
241
  if (isFirebase) {
242
242
  printCompactHeader(tr);
243
- ui.intro('Deploy — Firebase');
243
+ ui.intro(tr('deploy.firebase.intro'));
244
244
  await deployFirebase(projectDir, options, tr);
245
- ui.outro('Deploy concluído');
245
+ ui.outro(tr('deploy.outro'));
246
246
  return;
247
247
  }
248
248
 
249
249
  if (isSupabase) {
250
250
  printCompactHeader(tr);
251
- ui.intro('Deploy — Supabase');
252
- await deploySupabase(projectDir);
253
- ui.outro('Deploy concluído');
251
+ ui.intro(tr('deploy.supabase.intro'));
252
+ await deploySupabase(projectDir, tr);
253
+ ui.outro(tr('deploy.outro'));
254
254
  return;
255
255
  }
256
256
 
@@ -1,4 +1,13 @@
1
1
  {
2
+ "1.10.0": {
3
+ "modules": {
4
+ "widget": {
5
+ "pt": "Widget agora funciona ponta a ponta: idioma do app (pt/en/es), saudação correta pelo horário, nome real do usuário, status PRO/Free em tempo real (com fallback do RevenueCat quando o webhook atrasa), auto-refresh quando o estado do usuário muda — sem esperar o background task de 15 min. App Group iOS corrigido em Runner.entitlements (sem isso a escrita ia pro vazio). Textos hardcoded em Swift/Kotlin removidos — o nativo só renderiza o que o Dart manda.",
6
+ "en": "Widget now works end-to-end: app language (pt/en/es), correct greeting for time of day, real user name, real-time PRO/Free status (with RevenueCat fallback when the webhook is delayed), auto-refresh when user state changes — no need to wait for the 15-min background task. iOS App Group fixed in Runner.entitlements (without it, writes went to nowhere). Hardcoded strings removed from Swift/Kotlin — native side only renders what Dart sends.",
7
+ "es": "El widget ahora funciona de punta a punta: idioma de la app (pt/en/es), saludo correcto según el horario, nombre real del usuario, estado PRO/Free en tiempo real (con fallback de RevenueCat cuando el webhook se atrasa), auto-refresh cuando el estado del usuario cambia — sin esperar la background task de 15 min. App Group iOS corregido en Runner.entitlements (sin esto las escrituras iban al vacío). Textos hardcoded en Swift/Kotlin eliminados — el código nativo solo renderiza lo que Dart envía."
8
+ }
9
+ }
10
+ },
2
11
  "1.8.0": {
3
12
  "modules": {
4
13
  "components": {
@@ -31,7 +31,7 @@ Ao gerar um projeto, o engine copia recursivamente o conteúdo de `features/{mod
31
31
  |-------------|------------------|-----------------------------------------------------|
32
32
  | `ci` | ✅ Sim | Adiciona `.github/`, `.gitlab-ci.yml`, etc. |
33
33
  | `web` | ✅ Sim | Adiciona pasta `web/` e configurações de plataforma |
34
- | `widget` | ✅ Sim | Adiciona configurações Android para home widgets |
34
+ | `widget` | Não | Os arquivos do widget (iOS + Android + Dart) já vão no template base. Quando o módulo não é selecionado, `removeAndroidWidgetArtifacts` + `writeNoOpAdminHomeWidgets` em `generate.js` limpam o que sobra. |
35
35
  | `llm_chat` | Não | Apenas `LLM_CHAT_ENDPOINT` via dart-define. A chave da API LLM fica no servidor (Firebase Secret / Supabase Secret) — nunca no app. |
36
36
  | `sentry` | Não | Apenas dart-define (`SENTRY_DSN`) |
37
37
  | `analytics` | Não | Apenas dart-define |