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.
@@ -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
- spinner.fail(t('checks.missing', { name: check.name }) + detail);
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
- process.stdout.write(` ${kleur.dim(spinnerLabel + '...')}\n`);
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
- console.log(kleur.green(` ✔ ${doneLabel}`));
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 ${kleur.dim(`→ ${result.failHint}`)}` : '';
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
- console.log(kleur.red(` ✖ ${t('checks.missing', { name: result.name })}${detail}${hint}`));
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
- console.log(kleur.yellow(` ⚠ ${result.warnMessage}${hint}`));
327
+ ui.log.warn(`${result.warnMessage}${hint}`);
299
328
  } else if (result.warnMessageKey) {
300
- console.log(kleur.yellow(` ⚠ ${t(result.warnMessageKey)}${hint}`));
329
+ ui.log.warn(`${t(result.warnMessageKey)}${hint}`);
301
330
  } else {
302
- console.log(kleur.yellow(` ⚠ ${t('checks.notFound', { name: result.name })}${hint}`));
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.continueAnyway': 'Continue creating the project with the current version?',
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.continueAnyway': 'Continuar criando o projeto com a versao atual?',
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.continueAnyway': '¿Continuar creando el proyecto con la version actual?',
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
 
@@ -1,4 +1,4 @@
1
- const prompts = require('prompts');
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 getPromptOptions(t) {
19
- return {
20
- onCancel: () => {
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
- return normalizeLanguage(response.language) || detectedLanguage;
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 response = await prompts(
56
- {
57
- type: 'text',
58
- name: 'licenseKey',
59
- message: t('prompt.license.enter'),
60
- initial,
61
- validate: (value) => {
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 response = await prompts(
78
- {
79
- type: 'text',
80
- name: 'projectName',
81
- message: t('prompt.projectName.enter'),
82
- initial: t('prompt.projectName.default'),
83
- validate: (value) => (value && value.trim() ? true : t('prompt.projectName.required'))
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 baseQuestions = [
96
- {
97
- type: 'text',
98
- name: 'appName',
99
- message: t('prompt.appName.enter'),
100
- initial: defaultAppName,
101
- validate: (value) => (value && value.trim() ? true : t('prompt.appName.required'))
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
- type: 'text',
105
- name: 'bundleId',
106
- message: t('prompt.bundleId.enter'),
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
- if (!backendFromArgv) {
121
- baseQuestions.push({
122
- type: 'select',
123
- name: 'backend',
94
+ let backend = backendFromArgv;
95
+ if (!backend) {
96
+ backend = await ui.select({
124
97
  message: t('prompt.backend.select'),
125
- choices: AVAILABLE_BACKENDS.map((backend) => ({
126
- title: backend.id,
127
- value: backend.id
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
- if (selectedFeaturesFromArgv.length === 0) {
134
- baseQuestions.push({
135
- type: 'multiselect',
136
- name: 'features',
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
- instructions: t('prompt.features.instructions'),
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
- selected: false
112
+ label: feature.status === 'internal' ? `${feature.id} [beta]` : feature.id,
143
113
  })),
144
- min: 0
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 firebaseAnswers = await prompts(
156
- {
157
- type: 'text',
158
- name: 'firebaseProjectId',
159
- message: t('prompt.firebase.projectId.enter'),
160
- validate: (value) => (value && value.trim() ? true : t('prompt.firebase.projectId.required'))
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 supabaseAnswers = await prompts(
175
- [
176
- {
177
- type: 'text',
178
- name: 'supabaseUrl',
179
- message: t('prompt.supabase.url.enter'),
180
- validate: (value) => (value && value.trim() ? true : t('prompt.supabase.url.required'))
181
- },
182
- {
183
- type: 'text',
184
- name: 'supabaseAnonKey',
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 = {
@@ -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
+ };