kasy-cli 1.5.2 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/kasy.js +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 +662 -913
- package/lib/commands/notifications.js +59 -59
- package/lib/commands/remove.js +28 -27
- package/lib/commands/run.js +5 -2
- 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 +39 -10
- package/lib/utils/i18n.js +21 -3
- package/lib/utils/prompts.js +82 -142
- package/lib/utils/ui.js +174 -0
- package/lib/utils/updates.js +17 -18
- package/package.json +3 -1
package/lib/utils/checks.js
CHANGED
|
@@ -3,6 +3,7 @@ const { promisify } = require('node:util');
|
|
|
3
3
|
const readline = require('node:readline');
|
|
4
4
|
const kleur = require('kleur');
|
|
5
5
|
const oraPackage = require('ora');
|
|
6
|
+
const ui = require('./ui');
|
|
6
7
|
const { createTranslator, detectDefaultLanguage } = require('./i18n');
|
|
7
8
|
|
|
8
9
|
const execAsync = promisify(exec);
|
|
@@ -144,6 +145,17 @@ function getBackendChecks(backend) {
|
|
|
144
145
|
return [...(BACKEND_CHECKS[backend] || [])];
|
|
145
146
|
}
|
|
146
147
|
|
|
148
|
+
function diagnoseFailure(err) {
|
|
149
|
+
const text = `${err?.stderr || ''}\n${err?.stdout || ''}\n${err?.message || ''}`.toLowerCase();
|
|
150
|
+
if (text.includes('xcode') && text.includes('license')) {
|
|
151
|
+
return 'xcodeLicense';
|
|
152
|
+
}
|
|
153
|
+
if (text.includes('xcode-select') || text.includes('command line tools') || text.includes('no developer tools')) {
|
|
154
|
+
return 'xcodeCli';
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
147
159
|
function extractVersion(stdout, checkName) {
|
|
148
160
|
const raw = (stdout || '').trim();
|
|
149
161
|
if (!raw) return null;
|
|
@@ -200,7 +212,8 @@ async function runSingleCheck(check, options = {}) {
|
|
|
200
212
|
}
|
|
201
213
|
}
|
|
202
214
|
return { ...check, ok: true, version: version || null };
|
|
203
|
-
} catch {
|
|
215
|
+
} catch (err) {
|
|
216
|
+
const diagnosis = diagnoseFailure(err);
|
|
204
217
|
if (check.tryInstall) {
|
|
205
218
|
if (!silent) spinner.text = t(check.tryInstallMessageKey || 'setup.flutterfire.installing');
|
|
206
219
|
try {
|
|
@@ -242,7 +255,10 @@ async function runSingleCheck(check, options = {}) {
|
|
|
242
255
|
if (!silent) {
|
|
243
256
|
if (check.required) {
|
|
244
257
|
const detail = autoInstallFailed ? ` — ${t('checks.install.failed')}` : '';
|
|
245
|
-
|
|
258
|
+
const diagSuffix = diagnosis ? `\n ${kleur.dim(`→ ${t(`checks.diagnostic.${diagnosis}`, { name: check.name })}`)}` : '';
|
|
259
|
+
spinner.fail(t('checks.missing', { name: check.name }) + detail + diagSuffix);
|
|
260
|
+
} else if (diagnosis) {
|
|
261
|
+
spinner.warn(t(`checks.diagnostic.${diagnosis}`, { name: check.name }));
|
|
246
262
|
} else {
|
|
247
263
|
const hint = !check.waitPrompt && check.failHint ? `\n ${kleur.dim(`→ ${check.failHint}`)}` : '';
|
|
248
264
|
if (check.warnMessage) {
|
|
@@ -255,7 +271,7 @@ async function runSingleCheck(check, options = {}) {
|
|
|
255
271
|
}
|
|
256
272
|
}
|
|
257
273
|
|
|
258
|
-
return { ...check, ok: false, autoInstallFailed };
|
|
274
|
+
return { ...check, ok: false, autoInstallFailed, diagnosis };
|
|
259
275
|
}
|
|
260
276
|
}
|
|
261
277
|
|
|
@@ -275,7 +291,8 @@ async function runChecks(checks, title, options = {}) {
|
|
|
275
291
|
|
|
276
292
|
// Compact mode: single spinner, show failures only
|
|
277
293
|
const { spinnerLabel = title, doneLabel = title } = options;
|
|
278
|
-
|
|
294
|
+
const spinner = ui.spinner();
|
|
295
|
+
spinner.start(spinnerLabel);
|
|
279
296
|
|
|
280
297
|
const results = [];
|
|
281
298
|
for (const check of checks) {
|
|
@@ -283,23 +300,35 @@ async function runChecks(checks, title, options = {}) {
|
|
|
283
300
|
}
|
|
284
301
|
|
|
285
302
|
const failures = results.filter((r) => !r.ok);
|
|
303
|
+
const requiredFailures = failures.filter((r) => r.required);
|
|
286
304
|
|
|
287
305
|
if (failures.length === 0) {
|
|
288
|
-
|
|
306
|
+
spinner.stop(doneLabel);
|
|
289
307
|
return results;
|
|
290
308
|
}
|
|
291
309
|
|
|
310
|
+
// Close the spinner reflecting what actually happened: red ▲ if a required
|
|
311
|
+
// check failed, default green ✦ if only optional checks failed (warnings).
|
|
312
|
+
if (requiredFailures.length > 0) {
|
|
313
|
+
spinner.error(doneLabel);
|
|
314
|
+
} else {
|
|
315
|
+
spinner.stop(doneLabel);
|
|
316
|
+
}
|
|
317
|
+
|
|
292
318
|
for (const result of failures) {
|
|
293
|
-
const hint = result.failHint ? `\n
|
|
319
|
+
const hint = result.failHint ? `\n${kleur.dim(`→ ${result.failHint}`)}` : '';
|
|
294
320
|
if (result.required) {
|
|
295
321
|
const detail = result.autoInstallFailed ? ` — ${t('checks.install.failed')}` : '';
|
|
296
|
-
|
|
322
|
+
const diagSuffix = result.diagnosis ? `\n${kleur.dim(`→ ${t(`checks.diagnostic.${result.diagnosis}`, { name: result.name })}`)}` : '';
|
|
323
|
+
ui.log.error(`${t('checks.missing', { name: result.name })}${detail}${diagSuffix}${hint}`);
|
|
324
|
+
} else if (result.diagnosis) {
|
|
325
|
+
ui.log.warn(t(`checks.diagnostic.${result.diagnosis}`, { name: result.name }));
|
|
297
326
|
} else if (result.warnMessage) {
|
|
298
|
-
|
|
327
|
+
ui.log.warn(`${result.warnMessage}${hint}`);
|
|
299
328
|
} else if (result.warnMessageKey) {
|
|
300
|
-
|
|
329
|
+
ui.log.warn(`${t(result.warnMessageKey)}${hint}`);
|
|
301
330
|
} else {
|
|
302
|
-
|
|
331
|
+
ui.log.warn(`${t('checks.notFound', { name: result.name })}${hint}`);
|
|
303
332
|
}
|
|
304
333
|
}
|
|
305
334
|
|
package/lib/utils/i18n.js
CHANGED
|
@@ -96,6 +96,8 @@ const MESSAGES = {
|
|
|
96
96
|
'checks.notFound': '{name} not found',
|
|
97
97
|
'checks.flutter.warn': 'Flutter SDK not found. Install Flutter to build and run apps: https://docs.flutter.dev/get-started/install',
|
|
98
98
|
'checks.install.failed': 'auto-install failed — run the command manually',
|
|
99
|
+
'checks.diagnostic.xcodeLicense': '{name} is installed, but Xcode needs the license accepted. Run: sudo xcodebuild -license',
|
|
100
|
+
'checks.diagnostic.xcodeCli': '{name} is installed, but Xcode Command Line Tools are missing. Run: xcode-select --install',
|
|
99
101
|
'banner.title': 'Kasy CLI · Flutter SaaS Generator',
|
|
100
102
|
'welcome.firstRun': 'Welcome to Kasy CLI!',
|
|
101
103
|
'welcome.chooseLanguage': 'First, choose your language:',
|
|
@@ -551,7 +553,9 @@ const MESSAGES = {
|
|
|
551
553
|
'new.api.success.fcm': '• Firebase is required for push notifications (FCM) — configure APNs key in Firebase console',
|
|
552
554
|
'new.api.success.auth': '• Implement the social auth endpoints (Google, Apple) on your backend',
|
|
553
555
|
|
|
554
|
-
'new.outdated.
|
|
556
|
+
'new.outdated.hint': "projects created now won't include the latest improvements.",
|
|
557
|
+
'new.outdated.upgradeNow': 'Upgrade to the latest version before creating? (requires active subscription)',
|
|
558
|
+
'new.outdated.upgraded': 'kasy updated! Run kasy new again.',
|
|
555
559
|
'new.success.title': 'Project created successfully!',
|
|
556
560
|
'new.success.nextSteps': 'Next steps:',
|
|
557
561
|
'new.success.step.cd': 'Go to your project folder:',
|
|
@@ -584,6 +588,8 @@ const MESSAGES = {
|
|
|
584
588
|
// run command
|
|
585
589
|
'cli.command.run.description': '▶ Run the Flutter app using .env (launch.json dart-defines fallback)',
|
|
586
590
|
'run.launching': 'Launching Flutter app...',
|
|
591
|
+
'run.updateHint.prefix': 'Project improvements available —',
|
|
592
|
+
'run.updateHint.suffix': 'to see what\'s new',
|
|
587
593
|
'run.error.notFlutterProject': 'No pubspec.yaml found. Run this command from inside a Flutter project.',
|
|
588
594
|
'run.error.flutterNotFound': 'Flutter not found. Make sure Flutter is installed and on your PATH.',
|
|
589
595
|
|
|
@@ -815,6 +821,8 @@ const MESSAGES = {
|
|
|
815
821
|
'checks.notFound': '{name} nao encontrado',
|
|
816
822
|
'checks.flutter.warn': 'Flutter SDK nao encontrado. Instale o Flutter para compilar e executar apps: https://docs.flutter.dev/get-started/install',
|
|
817
823
|
'checks.install.failed': 'instalacao automatica falhou — execute o comando manualmente',
|
|
824
|
+
'checks.diagnostic.xcodeLicense': '{name} esta instalado, mas o Xcode precisa que a licenca seja aceita. Execute: sudo xcodebuild -license',
|
|
825
|
+
'checks.diagnostic.xcodeCli': '{name} esta instalado, mas faltam as Command Line Tools do Xcode. Execute: xcode-select --install',
|
|
818
826
|
'banner.title': 'Kasy CLI · Gerador Flutter SaaS',
|
|
819
827
|
'welcome.firstRun': 'Bem-vindo ao Kasy CLI!',
|
|
820
828
|
'welcome.chooseLanguage': 'Primeiro, escolha seu idioma:',
|
|
@@ -1270,7 +1278,9 @@ const MESSAGES = {
|
|
|
1270
1278
|
'new.api.success.fcm': '• Firebase e necessario para notificacoes push (FCM) — configure a chave APNs no console do Firebase',
|
|
1271
1279
|
'new.api.success.auth': '• Implemente os endpoints de auth social (Google, Apple) no seu backend',
|
|
1272
1280
|
|
|
1273
|
-
'new.outdated.
|
|
1281
|
+
'new.outdated.hint': 'projetos criados agora não terão as últimas melhorias.',
|
|
1282
|
+
'new.outdated.upgradeNow': 'Atualizar antes de criar? (requer assinatura ativa)',
|
|
1283
|
+
'new.outdated.upgraded': 'kasy atualizado! Rode kasy new novamente.',
|
|
1274
1284
|
'new.success.title': 'Projeto criado com sucesso!',
|
|
1275
1285
|
'new.success.nextSteps': 'Proximos passos:',
|
|
1276
1286
|
'new.success.step.cd': 'Entre na pasta do projeto:',
|
|
@@ -1303,6 +1313,8 @@ const MESSAGES = {
|
|
|
1303
1313
|
// run command
|
|
1304
1314
|
'cli.command.run.description': '▶ Executa o app Flutter usando .env (fallback para dart-defines do launch.json)',
|
|
1305
1315
|
'run.launching': 'Iniciando app Flutter...',
|
|
1316
|
+
'run.updateHint.prefix': 'Melhorias disponíveis para o projeto —',
|
|
1317
|
+
'run.updateHint.suffix': 'para ver o que há de novo',
|
|
1306
1318
|
'run.error.notFlutterProject': 'Nenhum pubspec.yaml encontrado. Execute este comando dentro de um projeto Flutter.',
|
|
1307
1319
|
'run.error.flutterNotFound': 'Flutter nao encontrado. Verifique se o Flutter esta instalado e no PATH.',
|
|
1308
1320
|
|
|
@@ -1534,6 +1546,8 @@ const MESSAGES = {
|
|
|
1534
1546
|
'checks.notFound': '{name} no encontrado',
|
|
1535
1547
|
'checks.flutter.warn': 'Flutter SDK no encontrado. Instala Flutter para compilar y ejecutar apps: https://docs.flutter.dev/get-started/install',
|
|
1536
1548
|
'checks.install.failed': 'instalación automática falló — ejecuta el comando manualmente',
|
|
1549
|
+
'checks.diagnostic.xcodeLicense': '{name} está instalado, pero Xcode necesita que aceptes la licencia. Ejecuta: sudo xcodebuild -license',
|
|
1550
|
+
'checks.diagnostic.xcodeCli': '{name} está instalado, pero faltan las Command Line Tools de Xcode. Ejecuta: xcode-select --install',
|
|
1537
1551
|
'banner.title': 'Kasy CLI · Generador Flutter SaaS',
|
|
1538
1552
|
'welcome.firstRun': '¡Bienvenido a Kasy CLI!',
|
|
1539
1553
|
'welcome.chooseLanguage': 'Primero, elige tu idioma:',
|
|
@@ -1989,7 +2003,9 @@ const MESSAGES = {
|
|
|
1989
2003
|
'new.api.success.fcm': '• Firebase es necesario para notificaciones push (FCM) — configura la clave APNs en la consola de Firebase',
|
|
1990
2004
|
'new.api.success.auth': '• Implementa los endpoints de auth social (Google, Apple) en tu backend',
|
|
1991
2005
|
|
|
1992
|
-
'new.outdated.
|
|
2006
|
+
'new.outdated.hint': 'los proyectos creados ahora no tendrán las últimas mejoras.',
|
|
2007
|
+
'new.outdated.upgradeNow': '¿Actualizar a la última versión antes de crear? (requiere suscripción activa)',
|
|
2008
|
+
'new.outdated.upgraded': '¡kasy actualizado! Ejecuta kasy new nuevamente.',
|
|
1993
2009
|
'new.success.title': '¡Proyecto creado con exito!',
|
|
1994
2010
|
'new.success.nextSteps': 'Proximos pasos:',
|
|
1995
2011
|
'new.success.step.cd': 'Ve a la carpeta del proyecto:',
|
|
@@ -2022,6 +2038,8 @@ const MESSAGES = {
|
|
|
2022
2038
|
// run command
|
|
2023
2039
|
'cli.command.run.description': '▶ Ejecuta el app Flutter usando .env (fallback a dart-defines del launch.json)',
|
|
2024
2040
|
'run.launching': 'Iniciando app Flutter...',
|
|
2041
|
+
'run.updateHint.prefix': 'Mejoras disponibles para el proyecto —',
|
|
2042
|
+
'run.updateHint.suffix': 'para ver las novedades',
|
|
2025
2043
|
'run.error.notFlutterProject': 'No se encontro pubspec.yaml. Ejecuta este comando dentro de un proyecto Flutter.',
|
|
2026
2044
|
'run.error.flutterNotFound': 'Flutter no encontrado. Verifica que Flutter este instalado y en el PATH.',
|
|
2027
2045
|
|
package/lib/utils/prompts.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const
|
|
1
|
+
const ui = require('./ui');
|
|
2
2
|
const { isLicenseFormatValid } = require('./license');
|
|
3
3
|
const {
|
|
4
4
|
LANGUAGE_CHOICES,
|
|
@@ -15,11 +15,9 @@ const {
|
|
|
15
15
|
|
|
16
16
|
const KASY_AUDIENCE = process.env.KASY_INTERNAL === '1' ? 'internal' : 'public';
|
|
17
17
|
|
|
18
|
-
function
|
|
19
|
-
return {
|
|
20
|
-
|
|
21
|
-
throw new Error(t('prompt.cancelled'));
|
|
22
|
-
}
|
|
18
|
+
function makeCancel(t) {
|
|
19
|
+
return () => {
|
|
20
|
+
throw new Error(t('prompt.cancelled'));
|
|
23
21
|
};
|
|
24
22
|
}
|
|
25
23
|
|
|
@@ -30,178 +28,120 @@ async function promptLanguage(language) {
|
|
|
30
28
|
}
|
|
31
29
|
|
|
32
30
|
const detectedLanguage = detectDefaultLanguage();
|
|
33
|
-
const initial = Math.max(
|
|
34
|
-
0,
|
|
35
|
-
LANGUAGE_CHOICES.findIndex((choice) => choice.value === detectedLanguage)
|
|
36
|
-
);
|
|
37
|
-
|
|
38
31
|
const t = createTranslator(detectedLanguage);
|
|
39
|
-
const response = await prompts(
|
|
40
|
-
{
|
|
41
|
-
type: 'select',
|
|
42
|
-
name: 'language',
|
|
43
|
-
message: t('prompt.language.select'),
|
|
44
|
-
choices: LANGUAGE_CHOICES,
|
|
45
|
-
initial
|
|
46
|
-
},
|
|
47
|
-
getPromptOptions(t)
|
|
48
|
-
);
|
|
49
32
|
|
|
50
|
-
|
|
33
|
+
const value = await ui.select({
|
|
34
|
+
message: t('prompt.language.select'),
|
|
35
|
+
initialValue: detectedLanguage,
|
|
36
|
+
options: LANGUAGE_CHOICES.map((choice) => ({
|
|
37
|
+
value: choice.value,
|
|
38
|
+
label: choice.title,
|
|
39
|
+
})),
|
|
40
|
+
onCancel: makeCancel(t),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return normalizeLanguage(value) || detectedLanguage;
|
|
51
44
|
}
|
|
52
45
|
|
|
53
46
|
async function promptLicenseKey(initial = '', options = {}) {
|
|
54
47
|
const t = options.t || createTranslator(options.language);
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if (isLicenseFormatValid(value)) {
|
|
63
|
-
return true;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return t('prompt.license.invalid');
|
|
67
|
-
}
|
|
68
|
-
},
|
|
69
|
-
getPromptOptions(t)
|
|
70
|
-
);
|
|
71
|
-
|
|
72
|
-
return response.licenseKey;
|
|
48
|
+
const value = await ui.text({
|
|
49
|
+
message: t('prompt.license.enter'),
|
|
50
|
+
initialValue: initial,
|
|
51
|
+
validate: (v) => (isLicenseFormatValid(v) ? undefined : t('prompt.license.invalid')),
|
|
52
|
+
onCancel: makeCancel(t),
|
|
53
|
+
});
|
|
54
|
+
return value;
|
|
73
55
|
}
|
|
74
56
|
|
|
75
57
|
async function promptProjectName(options = {}) {
|
|
76
58
|
const t = options.t || createTranslator(options.language);
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
},
|
|
85
|
-
getPromptOptions(t)
|
|
86
|
-
);
|
|
87
|
-
return response.projectName.trim();
|
|
59
|
+
const value = await ui.text({
|
|
60
|
+
message: t('prompt.projectName.enter'),
|
|
61
|
+
initialValue: t('prompt.projectName.default'),
|
|
62
|
+
validate: (v) => (v && v.trim() ? undefined : t('prompt.projectName.required')),
|
|
63
|
+
onCancel: makeCancel(t),
|
|
64
|
+
});
|
|
65
|
+
return String(value).trim();
|
|
88
66
|
}
|
|
89
67
|
|
|
90
68
|
async function runSetupWizard(options = {}) {
|
|
91
69
|
const t = options.t || createTranslator(options.language);
|
|
70
|
+
const cancel = makeCancel(t);
|
|
92
71
|
const defaultAppName = options.defaultAppName || 'MyApp';
|
|
93
72
|
const selectedFeaturesFromArgv = parseFeatureList(options.selectedFeatures);
|
|
94
73
|
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
74
|
+
const appName = await ui.text({
|
|
75
|
+
message: t('prompt.appName.enter'),
|
|
76
|
+
initialValue: defaultAppName,
|
|
77
|
+
validate: (v) => (v && v.trim() ? undefined : t('prompt.appName.required')),
|
|
78
|
+
onCancel: cancel,
|
|
79
|
+
});
|
|
80
|
+
const bundleId = await ui.text({
|
|
81
|
+
message: t('prompt.bundleId.enter'),
|
|
82
|
+
initialValue: 'com.example.app',
|
|
83
|
+
validate: (v) => {
|
|
84
|
+
if (!v || !v.trim()) return t('prompt.bundleId.required');
|
|
85
|
+
const valid = /^[a-zA-Z][\w]*(\.[a-zA-Z][\w]*)+$/.test(v.trim());
|
|
86
|
+
return valid ? undefined : t('prompt.bundleId.invalid');
|
|
102
87
|
},
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
initial: 'com.example.app',
|
|
108
|
-
validate: (value) => {
|
|
109
|
-
if (!value || !value.trim()) {
|
|
110
|
-
return t('prompt.bundleId.required');
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const valid = /^[a-zA-Z][\w]*(\.[a-zA-Z][\w]*)+$/.test(value.trim());
|
|
114
|
-
return valid ? true : t('prompt.bundleId.invalid');
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
];
|
|
88
|
+
onCancel: cancel,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const core = { appName, bundleId };
|
|
118
92
|
|
|
119
93
|
const backendFromArgv = normalizeBackend(options.selectedBackend);
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
name: 'backend',
|
|
94
|
+
let backend = backendFromArgv;
|
|
95
|
+
if (!backend) {
|
|
96
|
+
backend = await ui.select({
|
|
124
97
|
message: t('prompt.backend.select'),
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
})),
|
|
129
|
-
initial: 0
|
|
98
|
+
initialValue: AVAILABLE_BACKENDS[0]?.id,
|
|
99
|
+
options: AVAILABLE_BACKENDS.map((b) => ({ value: b.id, label: b.id })),
|
|
100
|
+
onCancel: cancel,
|
|
130
101
|
});
|
|
131
102
|
}
|
|
132
103
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
104
|
+
let features;
|
|
105
|
+
if (selectedFeaturesFromArgv.length > 0) {
|
|
106
|
+
features = selectedFeaturesFromArgv;
|
|
107
|
+
} else {
|
|
108
|
+
const selected = await ui.multiselect({
|
|
137
109
|
message: t('prompt.features.select'),
|
|
138
|
-
|
|
139
|
-
choices: getVisibleFeatures({ audience: KASY_AUDIENCE }).map((feature) => ({
|
|
140
|
-
title: feature.status === 'internal' ? `${feature.id} [beta]` : feature.id,
|
|
110
|
+
options: getVisibleFeatures({ audience: KASY_AUDIENCE }).map((feature) => ({
|
|
141
111
|
value: feature.id,
|
|
142
|
-
|
|
112
|
+
label: feature.status === 'internal' ? `${feature.id} [beta]` : feature.id,
|
|
143
113
|
})),
|
|
144
|
-
|
|
114
|
+
initialValues: [],
|
|
115
|
+
required: false,
|
|
116
|
+
onCancel: cancel,
|
|
145
117
|
});
|
|
118
|
+
features = parseFeatureList(selected);
|
|
146
119
|
}
|
|
147
120
|
|
|
148
|
-
const core = await prompts(baseQuestions, getPromptOptions(t));
|
|
149
|
-
const backend = backendFromArgv || core.backend;
|
|
150
|
-
const features = selectedFeaturesFromArgv.length > 0
|
|
151
|
-
? selectedFeaturesFromArgv
|
|
152
|
-
: parseFeatureList(core.features);
|
|
153
|
-
|
|
154
121
|
if (backend === 'firebase') {
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
},
|
|
162
|
-
getPromptOptions(t)
|
|
163
|
-
);
|
|
164
|
-
|
|
165
|
-
return {
|
|
166
|
-
...core,
|
|
167
|
-
backend,
|
|
168
|
-
features,
|
|
169
|
-
...firebaseAnswers
|
|
170
|
-
};
|
|
122
|
+
const firebaseProjectId = await ui.text({
|
|
123
|
+
message: t('prompt.firebase.projectId.enter'),
|
|
124
|
+
validate: (v) => (v && v.trim() ? undefined : t('prompt.firebase.projectId.required')),
|
|
125
|
+
onCancel: cancel,
|
|
126
|
+
});
|
|
127
|
+
return { ...core, backend, features, firebaseProjectId };
|
|
171
128
|
}
|
|
172
129
|
|
|
173
130
|
if (backend === 'supabase') {
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
message: t('prompt.supabase.anonKey.enter'),
|
|
186
|
-
validate: (value) => (value && value.trim() ? true : t('prompt.supabase.anonKey.required'))
|
|
187
|
-
}
|
|
188
|
-
],
|
|
189
|
-
getPromptOptions(t)
|
|
190
|
-
);
|
|
191
|
-
|
|
192
|
-
return {
|
|
193
|
-
...core,
|
|
194
|
-
backend,
|
|
195
|
-
features,
|
|
196
|
-
...supabaseAnswers
|
|
197
|
-
};
|
|
131
|
+
const supabaseUrl = await ui.text({
|
|
132
|
+
message: t('prompt.supabase.url.enter'),
|
|
133
|
+
validate: (v) => (v && v.trim() ? undefined : t('prompt.supabase.url.required')),
|
|
134
|
+
onCancel: cancel,
|
|
135
|
+
});
|
|
136
|
+
const supabaseAnonKey = await ui.text({
|
|
137
|
+
message: t('prompt.supabase.anonKey.enter'),
|
|
138
|
+
validate: (v) => (v && v.trim() ? undefined : t('prompt.supabase.anonKey.required')),
|
|
139
|
+
onCancel: cancel,
|
|
140
|
+
});
|
|
141
|
+
return { ...core, backend, features, supabaseUrl, supabaseAnonKey };
|
|
198
142
|
}
|
|
199
143
|
|
|
200
|
-
return {
|
|
201
|
-
...core,
|
|
202
|
-
backend,
|
|
203
|
-
features
|
|
204
|
-
};
|
|
144
|
+
return { ...core, backend, features };
|
|
205
145
|
}
|
|
206
146
|
|
|
207
147
|
module.exports = {
|
package/lib/utils/ui.js
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UI wrapper around @clack/prompts.
|
|
3
|
+
*
|
|
4
|
+
* Goal: centralize prompt/styling primitives so commands don't import
|
|
5
|
+
* `@clack/prompts` directly. This keeps the migration from the legacy
|
|
6
|
+
* `prompts` package incremental — commands can adopt this module one
|
|
7
|
+
* by one without touching the rest.
|
|
8
|
+
*
|
|
9
|
+
* Cancel handling: every prompt auto-detects Ctrl+C via `isCancel`
|
|
10
|
+
* and calls the optional `onCancel` callback (or exits with code 0
|
|
11
|
+
* and a friendly message). Callers don't need to wire `onCancel`
|
|
12
|
+
* in each call like they did with the old `prompts` library.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const clack = require('@clack/prompts');
|
|
16
|
+
|
|
17
|
+
function handleCancel(result, onCancel) {
|
|
18
|
+
if (clack.isCancel(result)) {
|
|
19
|
+
if (typeof onCancel === 'function') {
|
|
20
|
+
onCancel();
|
|
21
|
+
} else {
|
|
22
|
+
clack.cancel('Operação cancelada.');
|
|
23
|
+
}
|
|
24
|
+
process.exit(0);
|
|
25
|
+
}
|
|
26
|
+
return result;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function select({ message, options, initialValue, onCancel }) {
|
|
30
|
+
const result = await clack.select({ message, options, initialValue });
|
|
31
|
+
return handleCancel(result, onCancel);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function text({ message, placeholder, initialValue, defaultValue, validate, onCancel }) {
|
|
35
|
+
const result = await clack.text({
|
|
36
|
+
message,
|
|
37
|
+
placeholder,
|
|
38
|
+
initialValue,
|
|
39
|
+
defaultValue,
|
|
40
|
+
validate: validate
|
|
41
|
+
? (value) => {
|
|
42
|
+
const out = validate(value);
|
|
43
|
+
if (out === true || out == null) return undefined;
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
: undefined,
|
|
47
|
+
});
|
|
48
|
+
return handleCancel(result, onCancel);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function password({ message, mask, validate, onCancel }) {
|
|
52
|
+
const result = await clack.password({
|
|
53
|
+
message,
|
|
54
|
+
mask,
|
|
55
|
+
validate: validate
|
|
56
|
+
? (value) => {
|
|
57
|
+
const out = validate(value);
|
|
58
|
+
if (out === true || out == null) return undefined;
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
: undefined,
|
|
62
|
+
});
|
|
63
|
+
return handleCancel(result, onCancel);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function confirm({ message, initialValue = true, onCancel }) {
|
|
67
|
+
const result = await clack.confirm({ message, initialValue });
|
|
68
|
+
return handleCancel(result, onCancel);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function multiselect({ message, options, initialValues, required = false, onCancel }) {
|
|
72
|
+
const result = await clack.multiselect({ message, options, initialValues, required });
|
|
73
|
+
return handleCancel(result, onCancel);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function intro(title) { clack.intro(title); }
|
|
77
|
+
function outro(message) { clack.outro(message); }
|
|
78
|
+
function note(message, title) { clack.note(message, title); }
|
|
79
|
+
function cancel(message) { clack.cancel(message); }
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Spinner integrated with Clack's vertical line (│).
|
|
83
|
+
* Returns: { start(msg), message(msg), stop(msg, code?) }
|
|
84
|
+
* code: 0 = success (✦), 1 = cancel (■), 2 = error (▲)
|
|
85
|
+
*/
|
|
86
|
+
function spinner() { return clack.spinner(); }
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Multi-step spinner: each .next(text) succeeds the previous step
|
|
90
|
+
* with the previous message, then starts a new step with `text`.
|
|
91
|
+
* Mimics the old ora-based makeProgressSpinner() but uses Clack's
|
|
92
|
+
* vertical-line aesthetic so the line stays connected.
|
|
93
|
+
*/
|
|
94
|
+
function makeStepper() {
|
|
95
|
+
let current = null;
|
|
96
|
+
let currentMsg = '';
|
|
97
|
+
return {
|
|
98
|
+
next(text) {
|
|
99
|
+
if (current) current.stop(currentMsg);
|
|
100
|
+
current = clack.spinner();
|
|
101
|
+
currentMsg = text;
|
|
102
|
+
current.start(text);
|
|
103
|
+
},
|
|
104
|
+
update(text) {
|
|
105
|
+
if (current) {
|
|
106
|
+
currentMsg = text;
|
|
107
|
+
current.message(text);
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
succeed(text) {
|
|
111
|
+
if (current) {
|
|
112
|
+
current.stop(text || currentMsg);
|
|
113
|
+
current = null;
|
|
114
|
+
currentMsg = '';
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
fail(text) {
|
|
118
|
+
if (current) {
|
|
119
|
+
// .error() renders the red ▲ icon; .stop() defaults to green ✦ success.
|
|
120
|
+
current.error(text || currentMsg);
|
|
121
|
+
current = null;
|
|
122
|
+
currentMsg = '';
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
warn(text) {
|
|
126
|
+
// Clack has no "warn" terminal state — prefix with ⚠ and close as success.
|
|
127
|
+
if (current) {
|
|
128
|
+
current.stop(`⚠ ${text || currentMsg}`);
|
|
129
|
+
current = null;
|
|
130
|
+
currentMsg = '';
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
stop() {
|
|
134
|
+
if (current) {
|
|
135
|
+
current.stop(currentMsg);
|
|
136
|
+
current = null;
|
|
137
|
+
currentMsg = '';
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Long-running task with rolling log output (e.g. pub get, build_runner).
|
|
145
|
+
* Shows last N lines while running; collapses to a single ✦ on success.
|
|
146
|
+
* Options: { title, limit, retainLog }
|
|
147
|
+
*/
|
|
148
|
+
function taskLog(options) { return clack.taskLog(options); }
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Progress bar for operations with known total steps.
|
|
152
|
+
* Returns: { start(msg), advance(n, msg?), message(msg), stop(msg) }
|
|
153
|
+
*/
|
|
154
|
+
function progress(options) { return clack.progress(options); }
|
|
155
|
+
|
|
156
|
+
const log = clack.log;
|
|
157
|
+
|
|
158
|
+
module.exports = {
|
|
159
|
+
select,
|
|
160
|
+
text,
|
|
161
|
+
password,
|
|
162
|
+
confirm,
|
|
163
|
+
multiselect,
|
|
164
|
+
intro,
|
|
165
|
+
outro,
|
|
166
|
+
note,
|
|
167
|
+
cancel,
|
|
168
|
+
spinner,
|
|
169
|
+
makeStepper,
|
|
170
|
+
taskLog,
|
|
171
|
+
progress,
|
|
172
|
+
log,
|
|
173
|
+
isCancel: clack.isCancel,
|
|
174
|
+
};
|