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.
@@ -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,
@@ -6,20 +6,10 @@ const {
6
6
  detectDefaultLanguage,
7
7
  normalizeLanguage
8
8
  } = require('./i18n');
9
- const {
10
- AVAILABLE_BACKENDS,
11
- getVisibleFeatures,
12
- normalizeBackend,
13
- parseFeatureList
14
- } = require('../scaffold/catalog');
15
-
16
- const KASY_AUDIENCE = process.env.KASY_INTERNAL === '1' ? 'internal' : 'public';
17
9
 
18
- function getPromptOptions(t) {
19
- return {
20
- onCancel: () => {
21
- throw new Error(t('prompt.cancelled'));
22
- }
10
+ function makeCancel(t) {
11
+ return () => {
12
+ throw new Error(t('prompt.cancelled'));
23
13
  };
24
14
  }
25
15
 
@@ -30,178 +20,41 @@ async function promptLanguage(language) {
30
20
  }
31
21
 
32
22
  const detectedLanguage = detectDefaultLanguage();
33
- const initial = Math.max(
34
- 0,
35
- LANGUAGE_CHOICES.findIndex((choice) => choice.value === detectedLanguage)
36
- );
37
-
38
23
  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
24
 
50
- return normalizeLanguage(response.language) || detectedLanguage;
25
+ const value = await ui.select({
26
+ message: t('prompt.language.select'),
27
+ initialValue: detectedLanguage,
28
+ options: LANGUAGE_CHOICES.map((choice) => ({
29
+ value: choice.value,
30
+ label: choice.title,
31
+ })),
32
+ onCancel: makeCancel(t),
33
+ });
34
+
35
+ return normalizeLanguage(value) || detectedLanguage;
51
36
  }
52
37
 
53
38
  async function promptLicenseKey(initial = '', options = {}) {
54
39
  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;
40
+ const value = await ui.text({
41
+ message: t('prompt.license.enter'),
42
+ initialValue: initial,
43
+ validate: (v) => (isLicenseFormatValid(v) ? undefined : t('prompt.license.invalid')),
44
+ onCancel: makeCancel(t),
45
+ });
46
+ return value;
73
47
  }
74
48
 
75
49
  async function promptProjectName(options = {}) {
76
50
  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();
88
- }
89
-
90
- async function runSetupWizard(options = {}) {
91
- const t = options.t || createTranslator(options.language);
92
- const defaultAppName = options.defaultAppName || 'MyApp';
93
- const selectedFeaturesFromArgv = parseFeatureList(options.selectedFeatures);
94
-
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'))
102
- },
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
- ];
118
-
119
- const backendFromArgv = normalizeBackend(options.selectedBackend);
120
- if (!backendFromArgv) {
121
- baseQuestions.push({
122
- type: 'select',
123
- name: 'backend',
124
- message: t('prompt.backend.select'),
125
- choices: AVAILABLE_BACKENDS.map((backend) => ({
126
- title: backend.id,
127
- value: backend.id
128
- })),
129
- initial: 0
130
- });
131
- }
132
-
133
- if (selectedFeaturesFromArgv.length === 0) {
134
- baseQuestions.push({
135
- type: 'multiselect',
136
- name: 'features',
137
- 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,
141
- value: feature.id,
142
- selected: false
143
- })),
144
- min: 0
145
- });
146
- }
147
-
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
- 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
- };
171
- }
172
-
173
- 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
- };
198
- }
199
-
200
- return {
201
- ...core,
202
- backend,
203
- features
204
- };
51
+ const value = await ui.text({
52
+ message: t('prompt.projectName.enter'),
53
+ initialValue: t('prompt.projectName.default'),
54
+ validate: (v) => (v && v.trim() ? undefined : t('prompt.projectName.required')),
55
+ onCancel: makeCancel(t),
56
+ });
57
+ return String(value).trim();
205
58
  }
206
59
 
207
60
  module.exports = {
@@ -209,5 +62,4 @@ module.exports = {
209
62
  promptLicenseKey,
210
63
  promptLanguage,
211
64
  promptProjectName,
212
- runSetupWizard
213
65
  };
package/lib/utils/ui.js CHANGED
@@ -48,6 +48,21 @@ async function text({ message, placeholder, initialValue, defaultValue, validate
48
48
  return handleCancel(result, onCancel);
49
49
  }
50
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
+
51
66
  async function confirm({ message, initialValue = true, onCancel }) {
52
67
  const result = await clack.confirm({ message, initialValue });
53
68
  return handleCancel(result, onCancel);
@@ -63,13 +78,87 @@ function outro(message) { clack.outro(message); }
63
78
  function note(message, title) { clack.note(message, title); }
64
79
  function cancel(message) { clack.cancel(message); }
65
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
+ */
66
86
  function spinner() { return clack.spinner(); }
67
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
+
68
156
  const log = clack.log;
69
157
 
70
158
  module.exports = {
71
159
  select,
72
160
  text,
161
+ password,
73
162
  confirm,
74
163
  multiselect,
75
164
  intro,
@@ -77,6 +166,9 @@ module.exports = {
77
166
  note,
78
167
  cancel,
79
168
  spinner,
169
+ makeStepper,
170
+ taskLog,
171
+ progress,
80
172
  log,
81
173
  isCancel: clack.isCancel,
82
174
  };
@@ -4,8 +4,8 @@ const https = require('node:https');
4
4
  const path = require('node:path');
5
5
  const { existsSync, readJsonSync, writeJsonSync, ensureDirSync } = require('fs-extra');
6
6
  const kleur = require('kleur');
7
- const prompts = require('prompts');
8
7
  const pkg = require('../../package.json');
8
+ const ui = require('./ui');
9
9
  const { CONFIG_PATH } = require('./license');
10
10
 
11
11
  const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 horas
@@ -106,19 +106,20 @@ async function warnIfOutdatedBeforeNew(t) {
106
106
  if (!cache.latestVersion || !isNewer(cache.latestVersion, pkg.version)) return;
107
107
 
108
108
  const hint = t ? t('new.outdated.hint') : 'projetos criados agora não terão as últimas melhorias.';
109
- console.log('');
110
109
  console.log(
110
+ '\n' +
111
111
  kleur.bgYellow().black(' UPDATE ') + ' ' +
112
112
  kleur.bold(`v${cache.latestVersion} disponível`) +
113
- kleur.dim(` — ${hint}`)
113
+ kleur.dim(` — ${hint}`) +
114
+ '\n'
114
115
  );
115
- console.log('');
116
116
 
117
- const { upgrade } = await prompts({
118
- type: 'confirm',
119
- name: 'upgrade',
117
+ // Called BEFORE any ui.intro fires in kasy new, so this prompt runs as a
118
+ // standalone Clack confirm (no rail to break). On cancel we exit cleanly.
119
+ const upgrade = await ui.confirm({
120
120
  message: t ? t('new.outdated.upgradeNow') : 'Atualizar antes de criar? (requer assinatura ativa)',
121
- initial: false,
121
+ initialValue: false,
122
+ onCancel: () => process.exit(0),
122
123
  });
123
124
 
124
125
  if (upgrade) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kasy-cli",
3
- "version": "1.5.3",
3
+ "version": "1.7.0",
4
4
  "description": "CLI for scaffolding production-ready Flutter SaaS apps with Firebase, Supabase, or API REST backends.",
5
5
  "bin": {
6
6
  "kasy": "./bin/kasy.js"
@@ -45,6 +45,7 @@
45
45
  },
46
46
  "dependencies": {
47
47
  "@clack/prompts": "^1.4.0",
48
+ "boxen": "^8.0.1",
48
49
  "commander": "^12.0.0",
49
50
  "fs-extra": "^11.2.0",
50
51
  "gradient-string": "^1.2.0",
@@ -1,7 +1,5 @@
1
- import 'package:flutter/foundation.dart' show kDebugMode;
2
1
  import 'package:flutter/material.dart';
3
2
  import 'package:flutter_riverpod/flutter_riverpod.dart';
4
- import 'package:go_router/go_router.dart';
5
3
  import 'package:kasy_kit/components/kasy_app_bar.dart';
6
4
  import 'package:kasy_kit/core/bottom_menu/bart_inner_paths.dart';
7
5
  import 'package:kasy_kit/core/bottom_menu/kasy_bart_navigation.dart';
@@ -15,7 +13,6 @@ import 'package:kasy_kit/features/home/home_components_page.dart';
15
13
  import 'package:kasy_kit/features/home/home_features_page.dart';
16
14
  import 'package:kasy_kit/features/notifications/shared/att_permission.dart';
17
15
  import 'package:kasy_kit/features/notifications/shared/notification_permission_bottom_sheet.dart';
18
- import 'package:kasy_kit/features/settings/ui/components/admin/admin_routes.dart';
19
16
  import 'package:kasy_kit/features/subscription/shared/maybeshow_premium.dart';
20
17
  import 'package:kasy_kit/i18n/translations.g.dart';
21
18
 
@@ -118,22 +115,6 @@ class HomePage extends ConsumerWidget {
118
115
  );
119
116
  },
120
117
  ),
121
- if (kDebugMode) ...[
122
- const SizedBox(height: KasySpacing.md),
123
- FilledButton.icon(
124
- onPressed: () =>
125
- context.push(adminRouteKeyboardTest),
126
- icon: const Icon(Icons.keyboard, size: 18),
127
- label: const Text('Keyboard Test'),
128
- style: FilledButton.styleFrom(
129
- backgroundColor: Colors.deepOrange,
130
- minimumSize: const Size(double.infinity, 48),
131
- shape: RoundedRectangleBorder(
132
- borderRadius: BorderRadius.circular(12),
133
- ),
134
- ),
135
- ),
136
- ],
137
118
  const SizedBox(height: KasySpacing.xl),
138
119
  ]),
139
120
  ),
@@ -7,7 +7,6 @@ import 'package:kasy_kit/features/subscription/ui/component/premium_page_factory
7
7
  const String adminRoutePaywalls = '/admin/paywalls';
8
8
  const String adminRouteHomeWidgets = '/admin/home-widgets';
9
9
  const String adminRouteSendPush = '/admin/send-push';
10
- const String adminRouteKeyboardTest = '/admin/keyboard-test';
11
10
 
12
11
  String adminRoutePremiumPreview(String variant) => '/admin/premium/$variant';
13
12
 
@@ -14,7 +14,6 @@ import 'package:kasy_kit/features/authentication/ui/phone_auth_page.dart';
14
14
  import 'package:kasy_kit/features/authentication/ui/recover_password_page.dart';
15
15
  import 'package:kasy_kit/features/authentication/ui/signin_page.dart';
16
16
  import 'package:kasy_kit/features/authentication/ui/signup_page.dart';
17
- import 'package:kasy_kit/features/dev/keyboard_test_page.dart';
18
17
  import 'package:kasy_kit/features/feedbacks/ui/feedback_page.dart';
19
18
  import 'package:kasy_kit/features/llm_chat/llm_chat_page.dart';
20
19
  import 'package:kasy_kit/features/local_reminder/ui/reminder_page.dart';
@@ -205,14 +204,6 @@ GoRouter generateRouter({
205
204
  child: const SendPushNotificationPage(),
206
205
  ),
207
206
  ),
208
- GoRoute(
209
- name: 'keyboard_test',
210
- path: adminRouteKeyboardTest,
211
- pageBuilder: (context, state) => kasyTransitionPage(
212
- key: state.pageKey,
213
- child: const KeyboardTestPage(),
214
- ),
215
- ),
216
207
  GoRoute(
217
208
  name: '404',
218
209
  path: '/404',
@@ -1,93 +0,0 @@
1
- import 'package:flutter/material.dart';
2
- import 'package:kasy_kit/components/kasy_text_field.dart';
3
- import 'package:kasy_kit/core/theme/theme.dart';
4
-
5
- class KeyboardTestPage extends StatefulWidget {
6
- const KeyboardTestPage({super.key});
7
-
8
- @override
9
- State<KeyboardTestPage> createState() => _KeyboardTestPageState();
10
- }
11
-
12
- class _KeyboardTestPageState extends State<KeyboardTestPage> {
13
- final _name = TextEditingController();
14
- final _email = TextEditingController();
15
- final _phone = TextEditingController();
16
- final _password = TextEditingController();
17
- final _bio = TextEditingController();
18
-
19
- @override
20
- void dispose() {
21
- _name.dispose();
22
- _email.dispose();
23
- _phone.dispose();
24
- _password.dispose();
25
- _bio.dispose();
26
- super.dispose();
27
- }
28
-
29
- @override
30
- Widget build(BuildContext context) {
31
- return Scaffold(
32
- appBar: AppBar(title: const Text('Keyboard Test')),
33
- body: ListView(
34
- padding: EdgeInsets.fromLTRB(
35
- KasySpacing.pageHorizontalGutter,
36
- KasySpacing.md,
37
- KasySpacing.pageHorizontalGutter,
38
- MediaQuery.paddingOf(context).bottom + 40,
39
- ),
40
- children: [
41
- const Text(
42
- 'Tap between the fields — the keyboard should stay open without closing and reopening.',
43
- style: TextStyle(fontSize: 13, color: Colors.grey),
44
- ),
45
- const SizedBox(height: KasySpacing.lg),
46
- KasyTextField(
47
- controller: _name,
48
- label: 'Name',
49
- hint: 'Your full name',
50
- textInputAction: TextInputAction.next,
51
- textCapitalization: TextCapitalization.words,
52
- ),
53
- const SizedBox(height: KasyTextField.adjacentFieldSpacing),
54
- KasyTextField(
55
- controller: _email,
56
- label: 'Email',
57
- hint: 'you@example.com',
58
- contentType: KasyTextFieldContentType.email,
59
- keyboardType: TextInputType.emailAddress,
60
- textInputAction: TextInputAction.next,
61
- ),
62
- const SizedBox(height: KasyTextField.adjacentFieldSpacing),
63
- KasyTextField(
64
- controller: _phone,
65
- label: 'Phone',
66
- hint: '+55 11 99999-9999',
67
- contentType: KasyTextFieldContentType.phone,
68
- keyboardType: TextInputType.phone,
69
- textInputAction: TextInputAction.next,
70
- ),
71
- const SizedBox(height: KasyTextField.adjacentFieldSpacing),
72
- KasyTextField(
73
- controller: _password,
74
- label: 'Password',
75
- hint: 'Min 8 characters',
76
- contentType: KasyTextFieldContentType.password,
77
- textInputAction: TextInputAction.next,
78
- ),
79
- const SizedBox(height: KasyTextField.adjacentFieldSpacing),
80
- KasyTextField(
81
- controller: _bio,
82
- label: 'Bio',
83
- hint: 'Tell us about yourself',
84
- textInputAction: TextInputAction.done,
85
- minLines: 3,
86
- maxLines: 5,
87
- textCapitalization: TextCapitalization.sentences,
88
- ),
89
- ],
90
- ),
91
- );
92
- }
93
- }