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/lib/commands/update.js
CHANGED
|
@@ -6,8 +6,8 @@ const path = require('node:path');
|
|
|
6
6
|
const fs = require('fs-extra');
|
|
7
7
|
const pkg = require('../../package.json');
|
|
8
8
|
const kleur = require('kleur');
|
|
9
|
-
const
|
|
10
|
-
const
|
|
9
|
+
const ui = require('../utils/ui');
|
|
10
|
+
const { printCompactHeader } = require('../utils/brand');
|
|
11
11
|
const { createTranslator, detectDefaultLanguage } = require('../utils/i18n');
|
|
12
12
|
const {
|
|
13
13
|
AVAILABLE_FEATURES,
|
|
@@ -22,7 +22,6 @@ const { buildTokens } = require('../scaffold/backends/firebase/tokens');
|
|
|
22
22
|
const { localizeReleaseDocs } = require('../scaffold/shared/generator-utils');
|
|
23
23
|
|
|
24
24
|
const execAsync = promisify(exec);
|
|
25
|
-
const ora = oraPackage.default || oraPackage;
|
|
26
25
|
|
|
27
26
|
const CHANGELOG_PATH = path.join(__dirname, '..', 'scaffold', 'CHANGELOG.json');
|
|
28
27
|
|
|
@@ -146,6 +145,7 @@ async function getActiveModules(kitSetup, projectDir) {
|
|
|
146
145
|
async function runUpdate(module, options = {}) {
|
|
147
146
|
const t = createTranslator(options.language || detectDefaultLanguage());
|
|
148
147
|
const projectDir = path.resolve(options.directory || '.');
|
|
148
|
+
const cancel = () => { ui.cancel(t('update.cancelled')); process.exit(0); };
|
|
149
149
|
|
|
150
150
|
// 1. Must be inside a kasy project
|
|
151
151
|
const kitSetupPath = path.join(projectDir, 'kit_setup.json');
|
|
@@ -170,93 +170,98 @@ async function runUpdate(module, options = {}) {
|
|
|
170
170
|
if (module) {
|
|
171
171
|
const normalizedTarget = normalizeUpdateTarget(module);
|
|
172
172
|
if (normalizedTarget === COMPONENTS_UPDATE_TARGET) {
|
|
173
|
+
printCompactHeader(t);
|
|
174
|
+
ui.intro(t('update.applyingComponents'));
|
|
173
175
|
if (!options.yes) {
|
|
174
|
-
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
initial: false,
|
|
181
|
-
},
|
|
182
|
-
{ onCancel: () => { throw new Error(t('update.cancelled')); } }
|
|
183
|
-
);
|
|
176
|
+
ui.log.warn(t('update.warn.commitComponents'));
|
|
177
|
+
const confirmed = await ui.confirm({
|
|
178
|
+
message: t('update.confirmComponents'),
|
|
179
|
+
initialValue: false,
|
|
180
|
+
onCancel: cancel,
|
|
181
|
+
});
|
|
184
182
|
if (!confirmed) {
|
|
185
|
-
|
|
183
|
+
ui.outro(t('update.cancelled'));
|
|
186
184
|
return;
|
|
187
185
|
}
|
|
188
186
|
}
|
|
189
187
|
|
|
190
|
-
|
|
191
|
-
|
|
188
|
+
const spinner = ui.spinner();
|
|
189
|
+
spinner.start(t('update.applyingComponents'));
|
|
192
190
|
try {
|
|
193
191
|
const filesApplied = await applyBaseComponents(projectDir);
|
|
194
192
|
if (filesApplied === 0) {
|
|
195
|
-
spinner.
|
|
193
|
+
spinner.stop(`⚠ ${t('update.noComponentFiles')}`);
|
|
194
|
+
ui.outro(t('update.cancelled'));
|
|
196
195
|
return;
|
|
197
196
|
}
|
|
198
|
-
spinner.
|
|
197
|
+
spinner.stop(t('update.appliedComponents', { count: filesApplied }));
|
|
199
198
|
} catch (err) {
|
|
200
|
-
spinner.
|
|
199
|
+
spinner.error(t('update.applyComponentsFailed'));
|
|
201
200
|
throw err;
|
|
202
201
|
}
|
|
203
202
|
|
|
204
203
|
{
|
|
205
|
-
const spinnerPubGet =
|
|
204
|
+
const spinnerPubGet = ui.spinner();
|
|
205
|
+
spinnerPubGet.start(t('update.pubGet'));
|
|
206
206
|
try {
|
|
207
207
|
await execAsync('flutter pub get', { cwd: projectDir, timeout: 300_000 });
|
|
208
|
-
spinnerPubGet.
|
|
208
|
+
spinnerPubGet.stop(t('update.pubGetDone'));
|
|
209
209
|
} catch {
|
|
210
|
-
spinnerPubGet.
|
|
210
|
+
spinnerPubGet.stop(`⚠ ${t('update.pubGetFailed')}`);
|
|
211
211
|
}
|
|
212
212
|
}
|
|
213
213
|
|
|
214
214
|
kitSetup.cliVersion = currentVersion;
|
|
215
215
|
await fs.outputFile(kitSetupPath, JSON.stringify(kitSetup, null, 2) + '\n', 'utf8');
|
|
216
|
-
|
|
216
|
+
ui.outro(t('update.componentsSuccess'));
|
|
217
217
|
return;
|
|
218
218
|
}
|
|
219
219
|
|
|
220
220
|
if (normalizedTarget === CORE_UPDATE_TARGET) {
|
|
221
|
+
printCompactHeader(t);
|
|
222
|
+
ui.intro(t('update.applyingCore'));
|
|
221
223
|
if (!options.yes) {
|
|
222
|
-
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
224
|
+
ui.log.warn(t('update.warn.commitComponents'));
|
|
225
|
+
const confirmed = await ui.confirm({
|
|
226
|
+
message: t('update.confirmCore'),
|
|
227
|
+
initialValue: false,
|
|
228
|
+
onCancel: cancel,
|
|
229
|
+
});
|
|
227
230
|
if (!confirmed) {
|
|
228
|
-
|
|
231
|
+
ui.outro(t('update.cancelled'));
|
|
229
232
|
return;
|
|
230
233
|
}
|
|
231
234
|
}
|
|
232
235
|
|
|
233
|
-
|
|
234
|
-
|
|
236
|
+
const spinner = ui.spinner();
|
|
237
|
+
spinner.start(t('update.applyingCore'));
|
|
235
238
|
try {
|
|
236
239
|
const filesApplied = await applyCoreFiles(projectDir);
|
|
237
240
|
if (filesApplied === 0) {
|
|
238
|
-
spinner.
|
|
241
|
+
spinner.stop(`⚠ ${t('update.noComponentFiles')}`);
|
|
242
|
+
ui.outro(t('update.cancelled'));
|
|
239
243
|
return;
|
|
240
244
|
}
|
|
241
|
-
spinner.
|
|
245
|
+
spinner.stop(t('update.appliedCore', { count: filesApplied }));
|
|
242
246
|
} catch (err) {
|
|
243
|
-
spinner.
|
|
247
|
+
spinner.error(t('update.applyComponentsFailed'));
|
|
244
248
|
throw err;
|
|
245
249
|
}
|
|
246
250
|
|
|
247
251
|
{
|
|
248
|
-
const spinnerPubGet =
|
|
252
|
+
const spinnerPubGet = ui.spinner();
|
|
253
|
+
spinnerPubGet.start(t('update.pubGet'));
|
|
249
254
|
try {
|
|
250
255
|
await execAsync('flutter pub get', { cwd: projectDir, timeout: 300_000 });
|
|
251
|
-
spinnerPubGet.
|
|
256
|
+
spinnerPubGet.stop(t('update.pubGetDone'));
|
|
252
257
|
} catch {
|
|
253
|
-
spinnerPubGet.
|
|
258
|
+
spinnerPubGet.stop(`⚠ ${t('update.pubGetFailed')}`);
|
|
254
259
|
}
|
|
255
260
|
}
|
|
256
261
|
|
|
257
262
|
kitSetup.cliVersion = currentVersion;
|
|
258
263
|
await fs.outputFile(kitSetupPath, JSON.stringify(kitSetup, null, 2) + '\n', 'utf8');
|
|
259
|
-
|
|
264
|
+
ui.outro(t('update.coreSuccess'));
|
|
260
265
|
return;
|
|
261
266
|
}
|
|
262
267
|
|
|
@@ -265,24 +270,22 @@ async function runUpdate(module, options = {}) {
|
|
|
265
270
|
if (!(await fs.pathExists(patchDir))) {
|
|
266
271
|
throw new Error(t('update.error.noIosReleasePatch'));
|
|
267
272
|
}
|
|
273
|
+
printCompactHeader(t);
|
|
274
|
+
ui.intro(t('update.applying', { module: IOS_RELEASE_UPDATE_TARGET }));
|
|
268
275
|
if (!options.yes) {
|
|
269
|
-
|
|
270
|
-
const
|
|
271
|
-
{
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
initial: false,
|
|
276
|
-
},
|
|
277
|
-
{ onCancel: () => { throw new Error(t('update.cancelled')); } }
|
|
278
|
-
);
|
|
276
|
+
ui.log.warn(t('update.warn.commit', { module: IOS_RELEASE_UPDATE_TARGET }));
|
|
277
|
+
const confirmed = await ui.confirm({
|
|
278
|
+
message: t('update.confirm', { module: IOS_RELEASE_UPDATE_TARGET }),
|
|
279
|
+
initialValue: false,
|
|
280
|
+
onCancel: cancel,
|
|
281
|
+
});
|
|
279
282
|
if (!confirmed) {
|
|
280
|
-
|
|
283
|
+
ui.outro(t('update.cancelled'));
|
|
281
284
|
return;
|
|
282
285
|
}
|
|
283
286
|
}
|
|
284
|
-
|
|
285
|
-
|
|
287
|
+
const spinner = ui.spinner();
|
|
288
|
+
spinner.start(t('update.applying', { module: IOS_RELEASE_UPDATE_TARGET }));
|
|
286
289
|
try {
|
|
287
290
|
const { tokens, pathReplacements } = buildTokens({
|
|
288
291
|
appName: kitSetup.appName,
|
|
@@ -290,14 +293,14 @@ async function runUpdate(module, options = {}) {
|
|
|
290
293
|
});
|
|
291
294
|
await applyPatch(patchDir, projectDir, tokens, pathReplacements);
|
|
292
295
|
await localizeReleaseDocs(projectDir, options.language || detectDefaultLanguage());
|
|
293
|
-
spinner.
|
|
296
|
+
spinner.stop(t('update.iosRelease.success'));
|
|
294
297
|
} catch (err) {
|
|
295
|
-
spinner.
|
|
298
|
+
spinner.error(t('update.applyFailed', { module: IOS_RELEASE_UPDATE_TARGET }));
|
|
296
299
|
throw err;
|
|
297
300
|
}
|
|
298
301
|
kitSetup.cliVersion = currentVersion;
|
|
299
302
|
await fs.outputFile(kitSetupPath, JSON.stringify(kitSetup, null, 2) + '\n', 'utf8');
|
|
300
|
-
|
|
303
|
+
ui.outro(t('update.iosRelease.success'));
|
|
301
304
|
return;
|
|
302
305
|
}
|
|
303
306
|
|
|
@@ -311,74 +314,76 @@ async function runUpdate(module, options = {}) {
|
|
|
311
314
|
);
|
|
312
315
|
}
|
|
313
316
|
if (!activeModules.includes(normalized)) {
|
|
314
|
-
|
|
317
|
+
printCompactHeader(t);
|
|
318
|
+
ui.log.warn(t('update.error.notActive', { module: normalized }));
|
|
315
319
|
return;
|
|
316
320
|
}
|
|
317
321
|
|
|
318
322
|
const patchDir = path.join(FEATURES_PATCH_DIR, normalized);
|
|
319
323
|
if (!(await fs.pathExists(patchDir))) {
|
|
320
|
-
|
|
324
|
+
printCompactHeader(t);
|
|
325
|
+
ui.log.message(kleur.dim(t('update.noPatch', { module: normalized })));
|
|
321
326
|
return;
|
|
322
327
|
}
|
|
323
328
|
|
|
329
|
+
printCompactHeader(t);
|
|
330
|
+
ui.intro(t('update.applying', { module: normalized }));
|
|
331
|
+
|
|
324
332
|
// Warn and confirm
|
|
325
333
|
if (!options.yes) {
|
|
326
|
-
|
|
327
|
-
const
|
|
328
|
-
{
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
initial: false,
|
|
333
|
-
},
|
|
334
|
-
{ onCancel: () => { throw new Error(t('update.cancelled')); } }
|
|
335
|
-
);
|
|
334
|
+
ui.log.warn(t('update.warn.commit', { module: normalized }));
|
|
335
|
+
const confirmed = await ui.confirm({
|
|
336
|
+
message: t('update.confirm', { module: normalized }),
|
|
337
|
+
initialValue: false,
|
|
338
|
+
onCancel: cancel,
|
|
339
|
+
});
|
|
336
340
|
if (!confirmed) {
|
|
337
|
-
|
|
341
|
+
ui.outro(t('update.cancelled'));
|
|
338
342
|
return;
|
|
339
343
|
}
|
|
340
344
|
}
|
|
341
345
|
|
|
342
|
-
console.log('');
|
|
343
|
-
|
|
344
346
|
// Re-apply patch
|
|
345
347
|
{
|
|
346
|
-
const spinner =
|
|
348
|
+
const spinner = ui.spinner();
|
|
349
|
+
spinner.start(t('update.applying', { module: normalized }));
|
|
347
350
|
try {
|
|
348
351
|
const { tokens, pathReplacements } = buildTokens({
|
|
349
352
|
appName: kitSetup.appName,
|
|
350
353
|
bundleId: kitSetup.bundleId,
|
|
351
354
|
});
|
|
352
355
|
await applyPatch(patchDir, projectDir, tokens, pathReplacements);
|
|
353
|
-
spinner.
|
|
356
|
+
spinner.stop(t('update.applied', { module: normalized }));
|
|
354
357
|
} catch (err) {
|
|
355
|
-
spinner.
|
|
358
|
+
spinner.error(t('update.applyFailed', { module: normalized }));
|
|
356
359
|
throw err;
|
|
357
360
|
}
|
|
358
361
|
}
|
|
359
362
|
|
|
360
363
|
// flutter pub get
|
|
361
364
|
{
|
|
362
|
-
const spinner =
|
|
365
|
+
const spinner = ui.spinner();
|
|
366
|
+
spinner.start(t('update.pubGet'));
|
|
363
367
|
try {
|
|
364
368
|
await execAsync('flutter pub get', { cwd: projectDir, timeout: 300_000 });
|
|
365
|
-
spinner.
|
|
369
|
+
spinner.stop(t('update.pubGetDone'));
|
|
366
370
|
} catch {
|
|
367
|
-
spinner.
|
|
371
|
+
spinner.stop(`⚠ ${t('update.pubGetFailed')}`);
|
|
368
372
|
}
|
|
369
373
|
}
|
|
370
374
|
|
|
371
375
|
// build_runner (only for modules that generate code)
|
|
372
376
|
if (NEEDS_BUILD_RUNNER.includes(normalized)) {
|
|
373
|
-
const spinner =
|
|
377
|
+
const spinner = ui.spinner();
|
|
378
|
+
spinner.start(t('update.buildRunner'));
|
|
374
379
|
try {
|
|
375
380
|
await execAsync(
|
|
376
381
|
'dart run build_runner build --delete-conflicting-outputs',
|
|
377
382
|
{ cwd: projectDir, timeout: 600_000 }
|
|
378
383
|
);
|
|
379
|
-
spinner.
|
|
384
|
+
spinner.stop(t('update.buildRunnerDone'));
|
|
380
385
|
} catch {
|
|
381
|
-
spinner.
|
|
386
|
+
spinner.stop(`⚠ ${t('update.buildRunnerFailed')}`);
|
|
382
387
|
}
|
|
383
388
|
}
|
|
384
389
|
|
|
@@ -386,7 +391,7 @@ async function runUpdate(module, options = {}) {
|
|
|
386
391
|
kitSetup.cliVersion = currentVersion;
|
|
387
392
|
await fs.outputFile(kitSetupPath, JSON.stringify(kitSetup, null, 2) + '\n', 'utf8');
|
|
388
393
|
|
|
389
|
-
|
|
394
|
+
ui.outro(t('update.success', { module: normalized }));
|
|
390
395
|
return;
|
|
391
396
|
}
|
|
392
397
|
|
|
@@ -404,25 +409,26 @@ async function runUpdate(module, options = {}) {
|
|
|
404
409
|
}
|
|
405
410
|
|
|
406
411
|
if (alreadyUpToDate && Object.keys(changes).length === 0) {
|
|
407
|
-
|
|
412
|
+
printCompactHeader(t);
|
|
413
|
+
ui.outro(t('update.alreadyUpToDate', { version: currentVersion }));
|
|
408
414
|
return;
|
|
409
415
|
}
|
|
410
416
|
|
|
411
|
-
|
|
417
|
+
printCompactHeader(t);
|
|
412
418
|
const fromLabel = projectVersion || '?';
|
|
413
|
-
|
|
419
|
+
ui.intro(t('update.status', { from: fromLabel, to: currentVersion }));
|
|
414
420
|
|
|
415
421
|
// Modules/components with actual changelog entries — show what improved + the command
|
|
416
422
|
const modulesWithChanges = Object.keys(changes);
|
|
417
423
|
if (modulesWithChanges.length > 0) {
|
|
418
|
-
|
|
424
|
+
const lines = [kleur.bold(t('update.changesTitle'))];
|
|
419
425
|
for (const mod of modulesWithChanges) {
|
|
420
426
|
for (const { description } of changes[mod]) {
|
|
421
|
-
|
|
422
|
-
|
|
427
|
+
lines.push(`${kleur.cyan('✦')} ${kleur.bold(mod)} ${kleur.dim('→')} ${kleur.cyan(`kasy update ${mod}`)}`);
|
|
428
|
+
lines.push(` ${kleur.dim(description)}`);
|
|
423
429
|
}
|
|
424
430
|
}
|
|
425
|
-
|
|
431
|
+
ui.log.message(lines.join('\n'));
|
|
426
432
|
}
|
|
427
433
|
|
|
428
434
|
// Modules without changelog entries that can still be re-applied (advanced / recovery)
|
|
@@ -431,27 +437,29 @@ async function runUpdate(module, options = {}) {
|
|
|
431
437
|
|
|
432
438
|
if (modulesWithChanges.length === 0 && !hasComponentChanges) {
|
|
433
439
|
// Nothing new — show everything available
|
|
440
|
+
const lines = [];
|
|
434
441
|
if (patchableModules.length > 0) {
|
|
435
|
-
|
|
442
|
+
lines.push(t('update.howToUpdate'));
|
|
436
443
|
for (const m of patchableModules) {
|
|
437
|
-
|
|
444
|
+
lines.push(kleur.cyan(` kasy update ${m}`));
|
|
438
445
|
}
|
|
439
|
-
|
|
446
|
+
lines.push('');
|
|
440
447
|
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
448
|
+
lines.push(t('update.howToUpdateComponents'));
|
|
449
|
+
lines.push(kleur.cyan(` kasy update ${COMPONENTS_UPDATE_TARGET}`));
|
|
450
|
+
ui.log.message(lines.join('\n'));
|
|
444
451
|
} else if (modulesWithoutChanges.length > 0) {
|
|
445
452
|
// Some modules have no new changes — show as secondary info
|
|
446
|
-
|
|
453
|
+
const lines = [kleur.dim(t('update.reapplyTitle'))];
|
|
447
454
|
for (const m of modulesWithoutChanges) {
|
|
448
|
-
|
|
455
|
+
lines.push(kleur.dim(` kasy update ${m}`));
|
|
449
456
|
}
|
|
450
457
|
if (!hasComponentChanges) {
|
|
451
|
-
|
|
458
|
+
lines.push(kleur.dim(` kasy update ${COMPONENTS_UPDATE_TARGET}`));
|
|
452
459
|
}
|
|
453
|
-
|
|
460
|
+
ui.log.message(lines.join('\n'));
|
|
454
461
|
}
|
|
462
|
+
ui.outro('');
|
|
455
463
|
}
|
|
456
464
|
|
|
457
465
|
module.exports = { runUpdate };
|
package/lib/commands/validate.js
CHANGED
|
@@ -2,6 +2,8 @@ const path = require('node:path');
|
|
|
2
2
|
const { promisify } = require('node:util');
|
|
3
3
|
const { execFile } = require('node:child_process');
|
|
4
4
|
const kleur = require('kleur');
|
|
5
|
+
const ui = require('../utils/ui');
|
|
6
|
+
const { printCompactHeader } = require('../utils/brand');
|
|
5
7
|
const { createTranslator, detectDefaultLanguage } = require('../utils/i18n');
|
|
6
8
|
const { pathExists } = require('../utils/fs');
|
|
7
9
|
|
|
@@ -58,12 +60,16 @@ async function runValidate({ language, analyzeOnly = false } = {}) {
|
|
|
58
60
|
).then(() => true).catch(() => false);
|
|
59
61
|
|
|
60
62
|
if (!anyExists) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
+
printCompactHeader(t);
|
|
64
|
+
ui.intro(t('validate.title'));
|
|
65
|
+
ui.log.warn('kasy validate is only available in the Kasy development environment.');
|
|
66
|
+
ui.log.message(kleur.dim('Generate test projects with `kasy new` first, then run this command.'));
|
|
67
|
+
ui.outro('Skipped');
|
|
63
68
|
return;
|
|
64
69
|
}
|
|
65
70
|
|
|
66
|
-
|
|
71
|
+
printCompactHeader(t);
|
|
72
|
+
ui.intro(t('validate.title'));
|
|
67
73
|
|
|
68
74
|
const commands = buildCommandsForPlatform(analyzeOnly);
|
|
69
75
|
const failures = [];
|
|
@@ -75,54 +81,53 @@ async function runValidate({ language, analyzeOnly = false } = {}) {
|
|
|
75
81
|
if (!exists) {
|
|
76
82
|
const projectName = path.basename(combo.projectPath);
|
|
77
83
|
failures.push({ combo: combo.id, reason: `${t('validate.projectNotFound')}: ${combo.projectPath}` });
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
84
|
+
ui.log.error(label);
|
|
85
|
+
ui.log.message(
|
|
86
|
+
`${kleur.dim(combo.projectPath)}\n` +
|
|
81
87
|
kleur.yellow(
|
|
82
|
-
|
|
83
|
-
`
|
|
84
|
-
`
|
|
88
|
+
`⚠ Projeto base não encontrado. Gere-o antes de validar:\n` +
|
|
89
|
+
` kasy new ${projectName} --backend ${combo.backend}\n` +
|
|
90
|
+
` O projeto deve ser gerado uma vez e mantido localmente para CI.`
|
|
85
91
|
)
|
|
86
92
|
);
|
|
87
93
|
continue;
|
|
88
94
|
}
|
|
89
95
|
|
|
90
|
-
|
|
91
|
-
console.log(kleur.gray(` ${combo.projectPath}`));
|
|
96
|
+
ui.log.step(`${kleur.cyan('•')} ${label}\n${kleur.dim(combo.projectPath)}`);
|
|
92
97
|
|
|
93
98
|
let comboFailed = false;
|
|
94
99
|
for (const [cmd, args] of commands) {
|
|
95
100
|
const printable = `${cmd} ${args.join(' ')}`;
|
|
96
|
-
|
|
97
|
-
|
|
101
|
+
const spinner = ui.spinner();
|
|
102
|
+
spinner.start(printable);
|
|
98
103
|
try {
|
|
99
104
|
await runCommand(cmd, args, combo.projectPath);
|
|
100
|
-
|
|
105
|
+
spinner.stop(`${printable} ${kleur.green(t('validate.ok'))}`);
|
|
101
106
|
} catch (error) {
|
|
102
107
|
comboFailed = true;
|
|
103
108
|
failures.push({
|
|
104
109
|
combo: combo.id,
|
|
105
110
|
reason: `${printable} ${t('validate.fail')}`
|
|
106
111
|
});
|
|
107
|
-
|
|
112
|
+
spinner.error(`${printable} ${t('validate.fail')}`);
|
|
108
113
|
break;
|
|
109
114
|
}
|
|
110
115
|
}
|
|
111
116
|
|
|
112
117
|
if (!comboFailed) {
|
|
113
|
-
|
|
118
|
+
ui.log.success(`${combo.id} ${t('validate.passed')}`);
|
|
114
119
|
}
|
|
115
120
|
}
|
|
116
121
|
|
|
117
122
|
if (failures.length > 0) {
|
|
118
|
-
|
|
123
|
+
ui.log.error(t('validate.failed'));
|
|
119
124
|
failures.forEach((failure) => {
|
|
120
|
-
|
|
125
|
+
ui.log.error(`- ${failure.combo}: ${failure.reason}`);
|
|
121
126
|
});
|
|
122
127
|
throw new Error(t('validate.error'));
|
|
123
128
|
}
|
|
124
129
|
|
|
125
|
-
|
|
130
|
+
ui.outro(t('validate.success'));
|
|
126
131
|
}
|
|
127
132
|
|
|
128
133
|
module.exports = {
|
package/lib/scaffold/catalog.js
CHANGED
|
@@ -25,26 +25,58 @@ const AVAILABLE_BACKENDS = [
|
|
|
25
25
|
*
|
|
26
26
|
* availableIn: backends that support this feature
|
|
27
27
|
* defaultInPresets: which named presets include this feature by default
|
|
28
|
+
* displayName: user-facing label shown in CLI listings (Title Case, English)
|
|
29
|
+
* tag: optional restriction tag — `firebaseOnly` or `requiresDb` —
|
|
30
|
+
* rendered as a colored badge in CLI listings
|
|
31
|
+
* enhances: id of a base feature this optional feature activates real
|
|
32
|
+
* functionality for. Example: `revenuecat` enhances
|
|
33
|
+
* `subscription` — the Subscriptions screen exists in the base
|
|
34
|
+
* project but only sells real plans once RevenueCat is added.
|
|
35
|
+
*
|
|
36
|
+
* Note on id vs folder name (intentional, not a bug):
|
|
37
|
+
* - id `local_notifications` → folder `lib/features/local_reminder/`
|
|
38
|
+
* - id `feedback` → folder `lib/features/feedbacks/`
|
|
39
|
+
* The id describes the user-facing capability; the folder describes the
|
|
40
|
+
* Dart component. The generator (see generator-utils.js writeRouter and
|
|
41
|
+
* the cleanup step around feature dirs) removes these folders when the
|
|
42
|
+
* feature is not selected.
|
|
28
43
|
*/
|
|
29
44
|
const FEATURE_CATALOG = [
|
|
30
45
|
// integrations
|
|
31
|
-
{ id: 'sentry', status: 'public', availableIn: ['firebase', 'supabase', 'api'], defaultInPresets: ['starter', 'saas', 'content', 'full'] },
|
|
32
|
-
{ id: 'analytics', status: 'public', availableIn: ['firebase', 'supabase', 'api'], defaultInPresets: ['starter', 'saas', 'content', 'full'] },
|
|
33
|
-
{ id: 'facebook', status: 'public', availableIn: ['firebase', 'supabase', 'api'], defaultInPresets: ['full'] },
|
|
46
|
+
{ id: 'sentry', displayName: 'Crash Reports (Sentry)', status: 'public', availableIn: ['firebase', 'supabase', 'api'], defaultInPresets: ['starter', 'saas', 'content', 'full'] },
|
|
47
|
+
{ id: 'analytics', displayName: 'Analytics', status: 'public', availableIn: ['firebase', 'supabase', 'api'], defaultInPresets: ['starter', 'saas', 'content', 'full'] },
|
|
48
|
+
{ id: 'facebook', displayName: 'Facebook (Login + Ads)', status: 'public', availableIn: ['firebase', 'supabase', 'api'], defaultInPresets: ['full'] },
|
|
34
49
|
// monetization
|
|
35
|
-
{ id: 'revenuecat', status: 'public', availableIn: ['firebase', 'supabase', 'api'], defaultInPresets: ['saas', 'full'] },
|
|
50
|
+
{ id: 'revenuecat', displayName: 'RevenueCat', status: 'public', availableIn: ['firebase', 'supabase', 'api'], defaultInPresets: ['saas', 'full'], enhances: 'subscription' },
|
|
36
51
|
// features
|
|
37
|
-
{ id: 'onboarding', status: 'public', availableIn: ['firebase', 'supabase', 'api'], defaultInPresets: ['starter', 'saas', 'content', 'full'] },
|
|
38
|
-
{ id: 'web', status: 'public', availableIn: ['firebase'], defaultInPresets: [] },
|
|
39
|
-
{ id: 'widget', status: 'public', availableIn: ['firebase', 'supabase', 'api'], defaultInPresets: ['full'] },
|
|
40
|
-
{ id: 'llm_chat', status: 'public', availableIn: ['firebase', 'supabase', 'api'], defaultInPresets: ['content', 'full'] },
|
|
41
|
-
{ id: 'local_notifications', status: 'public', availableIn: ['firebase', 'supabase', 'api'], defaultInPresets: [] },
|
|
52
|
+
{ id: 'onboarding', displayName: 'Onboarding', status: 'public', availableIn: ['firebase', 'supabase', 'api'], defaultInPresets: ['starter', 'saas', 'content', 'full'] },
|
|
53
|
+
{ id: 'web', displayName: 'Web Support (PWA)', status: 'public', availableIn: ['firebase'], defaultInPresets: [], tag: 'firebaseOnly' },
|
|
54
|
+
{ id: 'widget', displayName: 'Home Widget', status: 'public', availableIn: ['firebase', 'supabase', 'api'], defaultInPresets: ['full'] },
|
|
55
|
+
{ id: 'llm_chat', displayName: 'AI Chat', status: 'public', availableIn: ['firebase', 'supabase', 'api'], defaultInPresets: ['content', 'full'] },
|
|
56
|
+
{ id: 'local_notifications', displayName: 'Local Reminders', status: 'public', availableIn: ['firebase', 'supabase', 'api'], defaultInPresets: [] },
|
|
42
57
|
// feedback (Firebase/Supabase only)
|
|
43
|
-
{ id: 'feedback', status: 'public', availableIn: ['firebase', 'supabase'], defaultInPresets: ['saas', 'full'] },
|
|
58
|
+
{ id: 'feedback', displayName: 'Feature Requests', status: 'public', availableIn: ['firebase', 'supabase'], defaultInPresets: ['saas', 'full'], tag: 'requiresDb' },
|
|
44
59
|
// ci/cd
|
|
45
|
-
{ id: 'ci', status: 'public', availableIn: ['firebase', 'supabase', 'api'], defaultInPresets: ['full'] },
|
|
60
|
+
{ id: 'ci', displayName: 'CI/CD', status: 'public', availableIn: ['firebase', 'supabase', 'api'], defaultInPresets: ['full'] },
|
|
46
61
|
];
|
|
47
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Base features — always present in every generated project.
|
|
65
|
+
* These cannot be added or removed via `kasy add` / `kasy remove`.
|
|
66
|
+
* Listed in the CLI so users understand what ships with every project.
|
|
67
|
+
*/
|
|
68
|
+
const BASE_FEATURES = [
|
|
69
|
+
{ id: 'authentication', displayName: 'Auth' },
|
|
70
|
+
{ id: 'home', displayName: 'Home Screen' },
|
|
71
|
+
{ id: 'settings', displayName: 'Settings Screen' },
|
|
72
|
+
{ id: 'notifications', displayName: 'Push Notifications' },
|
|
73
|
+
{ id: 'subscription', displayName: 'Subscriptions' },
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
function getBaseFeatures() {
|
|
77
|
+
return BASE_FEATURES;
|
|
78
|
+
}
|
|
79
|
+
|
|
48
80
|
/**
|
|
49
81
|
* Returns features visible to the given audience, optionally filtered by backend.
|
|
50
82
|
*
|
|
@@ -215,6 +247,7 @@ module.exports = {
|
|
|
215
247
|
FEATURES_PATCH_DIR,
|
|
216
248
|
AVAILABLE_BACKENDS,
|
|
217
249
|
FEATURE_CATALOG,
|
|
250
|
+
BASE_FEATURES,
|
|
218
251
|
AVAILABLE_FEATURES,
|
|
219
252
|
DEFAULT_FEATURES,
|
|
220
253
|
BASE_COMPONENT_FILES,
|
|
@@ -223,4 +256,5 @@ module.exports = {
|
|
|
223
256
|
normalizeFeature,
|
|
224
257
|
parseFeatureList,
|
|
225
258
|
getVisibleFeatures,
|
|
259
|
+
getBaseFeatures,
|
|
226
260
|
};
|
|
@@ -21,7 +21,7 @@ features/
|
|
|
21
21
|
...
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
Cada subdiretório tem o **mesmo nome** que o valor do módulo em `new.js` (`sentry`, `analytics`, `facebook`, `revenuecat`, `onboarding`, `web`, `widget`, `
|
|
24
|
+
Cada subdiretório tem o **mesmo nome** que o valor do módulo em `new.js` (`sentry`, `analytics`, `facebook`, `revenuecat`, `onboarding`, `web`, `widget`, `llm_chat`, `local_notifications`, `feedback`, `ci`).
|
|
25
25
|
|
|
26
26
|
Ao gerar um projeto, o engine copia recursivamente o conteúdo de `features/{modulo}/` para a raiz do projeto, sobrescrevendo ou acrescentando arquivos.
|
|
27
27
|
|
|
@@ -32,7 +32,6 @@ Ao gerar um projeto, o engine copia recursivamente o conteúdo de `features/{mod
|
|
|
32
32
|
| `ci` | ✅ Sim | Adiciona `.github/`, `.gitlab-ci.yml`, etc. |
|
|
33
33
|
| `web` | ✅ Sim | Adiciona pasta `web/` e configurações de plataforma |
|
|
34
34
|
| `widget` | ✅ Sim | Adiciona configurações Android para home widgets |
|
|
35
|
-
| `camera` | Opcional | Permissões extras no AndroidManifest / Info.plist |
|
|
36
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. |
|
|
37
36
|
| `sentry` | Não | Apenas dart-define (`SENTRY_DSN`) |
|
|
38
37
|
| `analytics` | Não | Apenas dart-define |
|
|
@@ -985,7 +985,7 @@ async function writeMainDart(projectDir, backend, modules, packageName, options
|
|
|
985
985
|
* directories for modules the user did not select to avoid dead code.
|
|
986
986
|
*
|
|
987
987
|
* @param {string} projectDir
|
|
988
|
-
* @param {string[]} modules - Selected modules (e.g. ['
|
|
988
|
+
* @param {string[]} modules - Selected modules (e.g. ['analytics', 'sentry'])
|
|
989
989
|
*/
|
|
990
990
|
async function removeModuleDirs(projectDir, modules) {
|
|
991
991
|
const removable = [
|