kasy-cli 1.5.3 → 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.
@@ -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 prompts = require('prompts');
10
- const oraPackage = require('ora');
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
- console.log(kleur.yellow(`\n ⚠ ${t('update.warn.commitComponents')}\n`));
175
- const { confirmed } = await prompts(
176
- {
177
- type: 'confirm',
178
- name: 'confirmed',
179
- message: t('update.confirmComponents'),
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
- console.log(kleur.dim(`\n${t('update.cancelled')}\n`));
183
+ ui.outro(t('update.cancelled'));
186
184
  return;
187
185
  }
188
186
  }
189
187
 
190
- console.log('');
191
- const spinner = ora(t('update.applyingComponents')).start();
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.warn(t('update.noComponentFiles'));
193
+ spinner.stop(`⚠ ${t('update.noComponentFiles')}`);
194
+ ui.outro(t('update.cancelled'));
196
195
  return;
197
196
  }
198
- spinner.succeed(t('update.appliedComponents', { count: filesApplied }));
197
+ spinner.stop(t('update.appliedComponents', { count: filesApplied }));
199
198
  } catch (err) {
200
- spinner.fail(t('update.applyComponentsFailed'));
199
+ spinner.error(t('update.applyComponentsFailed'));
201
200
  throw err;
202
201
  }
203
202
 
204
203
  {
205
- const spinnerPubGet = ora(t('update.pubGet')).start();
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.succeed(t('update.pubGetDone'));
208
+ spinnerPubGet.stop(t('update.pubGetDone'));
209
209
  } catch {
210
- spinnerPubGet.warn(t('update.pubGetFailed'));
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
- console.log(kleur.green(`\n✓ ${t('update.componentsSuccess')}\n`));
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
- console.log(kleur.yellow(`\n ⚠ ${t('update.warn.commitComponents')}\n`));
223
- const { confirmed } = await prompts(
224
- { type: 'confirm', name: 'confirmed', message: t('update.confirmCore'), initial: false },
225
- { onCancel: () => { throw new Error(t('update.cancelled')); } }
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
- console.log(kleur.dim(`\n${t('update.cancelled')}\n`));
231
+ ui.outro(t('update.cancelled'));
229
232
  return;
230
233
  }
231
234
  }
232
235
 
233
- console.log('');
234
- const spinner = ora(t('update.applyingCore')).start();
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.warn(t('update.noComponentFiles'));
241
+ spinner.stop(`⚠ ${t('update.noComponentFiles')}`);
242
+ ui.outro(t('update.cancelled'));
239
243
  return;
240
244
  }
241
- spinner.succeed(t('update.appliedCore', { count: filesApplied }));
245
+ spinner.stop(t('update.appliedCore', { count: filesApplied }));
242
246
  } catch (err) {
243
- spinner.fail(t('update.applyComponentsFailed'));
247
+ spinner.error(t('update.applyComponentsFailed'));
244
248
  throw err;
245
249
  }
246
250
 
247
251
  {
248
- const spinnerPubGet = ora(t('update.pubGet')).start();
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.succeed(t('update.pubGetDone'));
256
+ spinnerPubGet.stop(t('update.pubGetDone'));
252
257
  } catch {
253
- spinnerPubGet.warn(t('update.pubGetFailed'));
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
- console.log(kleur.green(`\n✓ ${t('update.coreSuccess')}\n`));
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
- console.log(kleur.yellow(`\n ⚠ ${t('update.warn.commit', { module: IOS_RELEASE_UPDATE_TARGET })}\n`));
270
- const { confirmed } = await prompts(
271
- {
272
- type: 'confirm',
273
- name: 'confirmed',
274
- message: t('update.confirm', { module: IOS_RELEASE_UPDATE_TARGET }),
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
- console.log(kleur.dim(`\n${t('update.cancelled')}\n`));
283
+ ui.outro(t('update.cancelled'));
281
284
  return;
282
285
  }
283
286
  }
284
- console.log('');
285
- const spinner = ora(t('update.applying', { module: IOS_RELEASE_UPDATE_TARGET })).start();
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.succeed(t('update.iosRelease.success'));
296
+ spinner.stop(t('update.iosRelease.success'));
294
297
  } catch (err) {
295
- spinner.fail(t('update.applyFailed', { module: IOS_RELEASE_UPDATE_TARGET }));
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
- console.log('');
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
- console.log(kleur.yellow(`\n${t('update.error.notActive', { module: normalized })}\n`));
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
- console.log(kleur.dim(`\n ${t('update.noPatch', { module: normalized })}\n`));
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
- console.log(kleur.yellow(`\n ⚠ ${t('update.warn.commit', { module: normalized })}\n`));
327
- const { confirmed } = await prompts(
328
- {
329
- type: 'confirm',
330
- name: 'confirmed',
331
- message: t('update.confirm', { module: normalized }),
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
- console.log(kleur.dim(`\n${t('update.cancelled')}\n`));
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 = ora(t('update.applying', { module: normalized })).start();
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.succeed(t('update.applied', { module: normalized }));
356
+ spinner.stop(t('update.applied', { module: normalized }));
354
357
  } catch (err) {
355
- spinner.fail(t('update.applyFailed', { module: normalized }));
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 = ora(t('update.pubGet')).start();
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.succeed(t('update.pubGetDone'));
369
+ spinner.stop(t('update.pubGetDone'));
366
370
  } catch {
367
- spinner.warn(t('update.pubGetFailed'));
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 = ora(t('update.buildRunner')).start();
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.succeed(t('update.buildRunnerDone'));
384
+ spinner.stop(t('update.buildRunnerDone'));
380
385
  } catch {
381
- spinner.warn(t('update.buildRunnerFailed'));
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
- console.log(kleur.green(`\n✓ ${t('update.success', { module: normalized })}\n`));
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
- console.log(kleur.green(`\n✓ ${t('update.alreadyUpToDate', { version: currentVersion })}\n`));
412
+ printCompactHeader(t);
413
+ ui.outro(t('update.alreadyUpToDate', { version: currentVersion }));
408
414
  return;
409
415
  }
410
416
 
411
- // Header
417
+ printCompactHeader(t);
412
418
  const fromLabel = projectVersion || '?';
413
- console.log(`\n${t('update.status', { from: fromLabel, to: currentVersion })}\n`);
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
- console.log(kleur.bold(t('update.changesTitle')));
424
+ const lines = [kleur.bold(t('update.changesTitle'))];
419
425
  for (const mod of modulesWithChanges) {
420
426
  for (const { description } of changes[mod]) {
421
- console.log(` ${kleur.cyan('✦')} ${kleur.bold(mod)} ${kleur.dim('→')} ${kleur.cyan(`kasy update ${mod}`)}`);
422
- console.log(` ${kleur.dim(description)}`);
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
- console.log('');
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
- console.log(t('update.howToUpdate'));
442
+ lines.push(t('update.howToUpdate'));
436
443
  for (const m of patchableModules) {
437
- console.log(kleur.cyan(` kasy update ${m}`));
444
+ lines.push(kleur.cyan(` kasy update ${m}`));
438
445
  }
439
- console.log('');
446
+ lines.push('');
440
447
  }
441
- console.log(t('update.howToUpdateComponents'));
442
- console.log(kleur.cyan(` kasy update ${COMPONENTS_UPDATE_TARGET}`));
443
- console.log('');
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
- console.log(kleur.dim(t('update.reapplyTitle')));
453
+ const lines = [kleur.dim(t('update.reapplyTitle'))];
447
454
  for (const m of modulesWithoutChanges) {
448
- console.log(kleur.dim(` kasy update ${m}`));
455
+ lines.push(kleur.dim(` kasy update ${m}`));
449
456
  }
450
457
  if (!hasComponentChanges) {
451
- console.log(kleur.dim(` kasy update ${COMPONENTS_UPDATE_TARGET}`));
458
+ lines.push(kleur.dim(` kasy update ${COMPONENTS_UPDATE_TARGET}`));
452
459
  }
453
- console.log('');
460
+ ui.log.message(lines.join('\n'));
454
461
  }
462
+ ui.outro('');
455
463
  }
456
464
 
457
465
  module.exports = { runUpdate };
@@ -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
- console.log(kleur.yellow('\n⚠ kasy validate is only available in the Kasy development environment.'));
62
- console.log(kleur.gray(' Generate test projects with `kasy new` first, then run this command.\n'));
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
- console.log(kleur.bold(`\n${t('validate.title')}\n`));
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
- console.log(kleur.red(`✗ ${label}`));
79
- console.log(kleur.gray(` ${combo.projectPath}`));
80
- console.log(
84
+ ui.log.error(label);
85
+ ui.log.message(
86
+ `${kleur.dim(combo.projectPath)}\n` +
81
87
  kleur.yellow(
82
- ` ⚠ Projeto base não encontrado. Gere-o antes de validar:\n` +
83
- ` kasy new ${projectName} --backend ${combo.backend}\n` +
84
- ` O projeto deve ser gerado uma vez e mantido localmente para CI.`
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
- console.log(kleur.cyan(`• ${label}`));
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
- process.stdout.write(kleur.gray(` - ${printable} ... `));
97
-
101
+ const spinner = ui.spinner();
102
+ spinner.start(printable);
98
103
  try {
99
104
  await runCommand(cmd, args, combo.projectPath);
100
- process.stdout.write(`${kleur.green(t('validate.ok'))}\n`);
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
- process.stdout.write(`${kleur.red(t('validate.fail'))}\n`);
112
+ spinner.error(`${printable} ${t('validate.fail')}`);
108
113
  break;
109
114
  }
110
115
  }
111
116
 
112
117
  if (!comboFailed) {
113
- console.log(kleur.green(` ✓ ${combo.id} ${t('validate.passed')}`));
118
+ ui.log.success(`${combo.id} ${t('validate.passed')}`);
114
119
  }
115
120
  }
116
121
 
117
122
  if (failures.length > 0) {
118
- console.log(kleur.red(`\n${t('validate.failed')}`));
123
+ ui.log.error(t('validate.failed'));
119
124
  failures.forEach((failure) => {
120
- console.log(kleur.red(`- ${failure.combo}: ${failure.reason}`));
125
+ ui.log.error(`- ${failure.combo}: ${failure.reason}`);
121
126
  });
122
127
  throw new Error(t('validate.error'));
123
128
  }
124
129
 
125
- console.log(kleur.green(`\n${t('validate.success')}`));
130
+ ui.outro(t('validate.success'));
126
131
  }
127
132
 
128
133
  module.exports = {
@@ -4,6 +4,7 @@ const path = require('node:path');
4
4
  const fs = require('fs-extra');
5
5
  const os = require('node:os');
6
6
  const kleur = require('kleur');
7
+ const ui = require('./ui');
7
8
  const { exec } = require('node:child_process');
8
9
  const { promisify } = require('node:util');
9
10
 
@@ -202,17 +203,21 @@ function isXcodeCacheBuildError(output) {
202
203
 
203
204
  function printBuildFailureHints(t, projectDir) {
204
205
  const name = path.basename(projectDir);
205
- console.log(kleur.yellow(`\n${t('ios.hints.title')}\n`));
206
- console.log(kleur.dim(` 1. ${t('ios.hints.closeXcode')}`));
207
- console.log(kleur.cyan(` 2. kasy ios clean${projectDir !== process.cwd() ? ` ${projectDir}` : ''}`));
208
- console.log(kleur.dim(` 3. ${t('ios.hints.retry')}: kasy ios release\n`));
206
+ const cleanArg = projectDir !== process.cwd() ? ` ${projectDir}` : '';
207
+ const lines = [
208
+ `1. ${t('ios.hints.closeXcode')}`,
209
+ `2. ${kleur.cyan(`kasy ios clean${cleanArg}`)}`,
210
+ `3. ${t('ios.hints.retry')}: ${kleur.cyan('kasy ios release')}`,
211
+ ];
209
212
  if (name) {
210
- console.log(kleur.dim(` ${t('ios.hints.manual')}:`));
211
- console.log(kleur.dim(` cd ${projectDir}`));
212
- console.log(kleur.dim(' flutter clean && flutter pub get'));
213
- console.log(kleur.dim(' rm -rf ~/Library/Developer/Xcode/DerivedData/Runner-*'));
214
- console.log(kleur.dim(' cd ios && pod install\n'));
213
+ lines.push('');
214
+ lines.push(kleur.dim(`${t('ios.hints.manual')}:`));
215
+ lines.push(kleur.dim(` cd ${projectDir}`));
216
+ lines.push(kleur.dim(' flutter clean && flutter pub get'));
217
+ lines.push(kleur.dim(' rm -rf ~/Library/Developer/Xcode/DerivedData/Runner-*'));
218
+ lines.push(kleur.dim(' cd ios && pod install'));
215
219
  }
220
+ ui.note(lines.join('\n'), t('ios.hints.title'));
216
221
  }
217
222
 
218
223
  async function runIosClean(projectDir, t) {
@@ -224,10 +229,17 @@ async function runIosClean(projectDir, t) {
224
229
  { label: t('ios.clean.step.pods'), cmd: 'cd ios && pod install' },
225
230
  ];
226
231
 
232
+ const stepper = ui.makeStepper();
227
233
  for (const step of steps) {
228
- console.log(kleur.dim(` ${step.label}…`));
229
- await execAsync(step.cmd, { cwd: projectDir, maxBuffer: 50 * 1024 * 1024, shell: '/bin/bash' });
234
+ stepper.next(step.label);
235
+ try {
236
+ await execAsync(step.cmd, { cwd: projectDir, maxBuffer: 50 * 1024 * 1024, shell: '/bin/bash' });
237
+ } catch (err) {
238
+ stepper.fail(`${step.label}: ${err.message}`);
239
+ throw err;
240
+ }
230
241
  }
242
+ stepper.succeed();
231
243
  }
232
244
 
233
245
  async function runReleaseScript(projectDir, args, t) {
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Kasy brand kit for the terminal.
3
+ *
4
+ * Centralizes logo, banner, headers, and boxed messages so every
5
+ * command projects a consistent identity. Two header sizes:
6
+ * - printBanner(): full ASCII logo + gradient + tagline (kasy new, kasy add)
7
+ * - printCompactHeader(): one-line ✦ KASY + tagline (kasy doctor, upgrade, run)
8
+ *
9
+ * Box helpers wrap `boxen` so commands stop drawing ─── lines by hand.
10
+ */
11
+
12
+ const kleur = require('kleur');
13
+ const gradient = require('gradient-string');
14
+ const boxenPackage = require('boxen');
15
+ const boxen = boxenPackage.default || boxenPackage;
16
+
17
+ // Kept around for accents and boxes; the wordmark itself is now plain white.
18
+ const BRAND_GRADIENT = ['#a78bfa', '#60a5fa'];
19
+ const brandGradient = gradient(BRAND_GRADIENT);
20
+
21
+ const DOMAIN_SUFFIX = kleur.gray('.dev');
22
+
23
+ function printBanner(_tr) {
24
+ const bar = kleur.gray('─────────────────────────────────────────────────');
25
+ const logo = [
26
+ kleur.white(' ╦╔═ ╔═╗ ╔═╗ ╦ ╦'),
27
+ kleur.white(' ╠╩╗ ╠═╣ ╚═╗ ╚╦╝'),
28
+ `${kleur.white(' ╩ ╩ ╩ ╩ ╚═╝ ╩ ')} ${DOMAIN_SUFFIX}`,
29
+ ].join('\n');
30
+
31
+ console.log(`\n${bar}\n`);
32
+ console.log(logo);
33
+ console.log(`\n${bar}\n`);
34
+ }
35
+
36
+ function printCompactHeader(_tr) {
37
+ console.log('');
38
+ console.log(` ${kleur.white('✦ KASY')}${DOMAIN_SUFFIX}`);
39
+ console.log('');
40
+ }
41
+
42
+ function successBox(title, body, { padding = 1, marginTop = 1, marginBottom = 1 } = {}) {
43
+ return boxen(
44
+ `${brandGradient(`✦ ${title}`)}\n\n${body}`,
45
+ {
46
+ padding,
47
+ margin: { top: marginTop, bottom: marginBottom, left: 1, right: 1 },
48
+ borderStyle: 'round',
49
+ borderColor: 'cyan',
50
+ }
51
+ );
52
+ }
53
+
54
+ function infoBox(title, body, { padding = 1, marginTop = 1, marginBottom = 1 } = {}) {
55
+ return boxen(
56
+ `${kleur.bold(title)}\n\n${body}`,
57
+ {
58
+ padding,
59
+ margin: { top: marginTop, bottom: marginBottom, left: 1, right: 1 },
60
+ borderStyle: 'round',
61
+ borderColor: 'gray',
62
+ }
63
+ );
64
+ }
65
+
66
+ module.exports = {
67
+ brandGradient,
68
+ printBanner,
69
+ printCompactHeader,
70
+ successBox,
71
+ infoBox,
72
+ };