From ea8baa41d46078d91a5c2d5bcadce7f6ff006683 Mon Sep 17 00:00:00 2001 From: Manish Sharma Date: Thu, 25 Jun 2026 18:15:17 +0530 Subject: [PATCH 1/2] fix(cli): ASCII fallback for emoji/box-drawing when NO_COLOR is set Terminals without unicode/emoji support render garbled CLI output. Add icons/BOX char maps in branding.ts gated on NO_COLOR env var, wire through dev/init/start commands. Fixes #25 --- typescript/packages/cli/src/commands/dev.ts | 23 ++--- typescript/packages/cli/src/commands/init.ts | 5 +- typescript/packages/cli/src/commands/start.ts | 5 +- typescript/packages/cli/src/ui/branding.ts | 84 ++++++++++++------- 4 files changed, 73 insertions(+), 44 deletions(-) diff --git a/typescript/packages/cli/src/commands/dev.ts b/typescript/packages/cli/src/commands/dev.ts index 3ccd35b..99a4c37 100644 --- a/typescript/packages/cli/src/commands/dev.ts +++ b/typescript/packages/cli/src/commands/dev.ts @@ -15,7 +15,8 @@ import { spacer, brand, nextSteps, - showFooter + showFooter, + icons, } from '../ui/branding.js'; import { trackEvent, shutdownAnalytics } from '../analytics/posthog.js'; @@ -93,7 +94,7 @@ class DevUI { const elapsed = ((Date.now() - this.startTime) / 1000).toFixed(1); const lines = [ - chalk.green.bold('✓ Development Server Ready'), + chalk.green.bold(`${icons.ok} Development Server Ready`), '', `${chalk.white.bold('MCP Server')} ${chalk.dim('Running (STDIO transport)')}`, ...(config.hasWidgets ? [`${chalk.white.bold('Widgets')} ${chalk.cyan(`http://localhost:${config.widgetsPort}`)}`] : []), @@ -118,10 +119,10 @@ class DevUI { log(message: string, type: 'info' | 'success' | 'warn' | 'error' = 'info'): void { const prefix = { - info: chalk.blue('ℹ'), - success: chalk.green('✓'), - warn: chalk.yellow('⚠'), - error: chalk.red('✗'), + info: chalk.blue(icons.info), + success: chalk.green(icons.ok), + warn: chalk.yellow(icons.warn), + error: chalk.red(icons.err), }[type]; console.log(`${prefix} ${message}`); } @@ -211,7 +212,7 @@ export async function devCommand(options: DevOptions) { } setTimeout(() => { - console.log(chalk.dim('\nGoodbye! 👋\n')); + console.log(chalk.dim(`\nGoodbye!${icons.wave}\n`)); process.exit(code); }, 500); }; @@ -320,10 +321,10 @@ export async function devCommand(options: DevOptions) { if (!tscReady) { tscReady = true; } else { - console.log(chalk.green('✓') + chalk.dim(' MCP server recompiled')); + console.log(chalk.green(icons.ok) + chalk.dim(' MCP server recompiled')); } } else if (output.includes('error TS')) { - console.log(chalk.red('✗') + chalk.dim(' TypeScript error - check your code')); + console.log(chalk.red(icons.err) + chalk.dim(' TypeScript error - check your code')); } }); } @@ -381,7 +382,7 @@ export async function devCommand(options: DevOptions) { const now = Date.now(); if (now - lastRestart < RESTART_COOLDOWN) return; - console.log(chalk.yellow('⚡') + chalk.dim(' New widget detected, restarting...')); + console.log(chalk.yellow(icons.bolt) + chalk.dim(' New widget detected, restarting...')); lastRestart = Date.now(); @@ -423,7 +424,7 @@ export async function devCommand(options: DevOptions) { widgetsDevProcess?.stderr?.on('data', (data) => { const output = data.toString(); if (output.includes('error')) { - console.log(chalk.red('✗') + chalk.dim(' Widgets: ') + output.trim()); + console.log(chalk.red(icons.err) + chalk.dim(' Widgets: ') + output.trim()); } }); diff --git a/typescript/packages/cli/src/commands/init.ts b/typescript/packages/cli/src/commands/init.ts index 4556c27..071e546 100644 --- a/typescript/packages/cli/src/commands/init.ts +++ b/typescript/packages/cli/src/commands/init.ts @@ -15,7 +15,8 @@ import { spacer, nextSteps, brand, - showFooter + showFooter, + icons, } from '../ui/branding.js'; import { trackEvent, shutdownAnalytics } from '../analytics/posthog.js'; @@ -273,7 +274,7 @@ export async function initCommand(projectName: string | undefined, options: Init console.log(chalk.dim(' Mapbox (optional): Get free key from mapbox.com\n')); } - console.log(chalk.dim(' Happy coding! 🎉\n')); + console.log(chalk.dim(` Happy coding!${icons.party}\n`)); showFooter(); trackEvent('cli_init_completed', { diff --git a/typescript/packages/cli/src/commands/start.ts b/typescript/packages/cli/src/commands/start.ts index 4fed3a4..d25dbd4 100644 --- a/typescript/packages/cli/src/commands/start.ts +++ b/typescript/packages/cli/src/commands/start.ts @@ -11,7 +11,8 @@ import { spacer, brand, NITRO_BANNER_FULL, - showFooter + showFooter, + icons, } from '../ui/branding.js'; import { trackEvent, shutdownAnalytics } from '../analytics/posthog.js'; @@ -97,7 +98,7 @@ export async function startCommand(options: StartOptions) { serverProcess.kill('SIGTERM'); setTimeout(() => { serverProcess.kill('SIGKILL'); - console.log(chalk.dim('\nGoodbye! 👋\n')); + console.log(chalk.dim(`\nGoodbye!${icons.wave}\n`)); process.exit(0); }, 5000); }; diff --git a/typescript/packages/cli/src/ui/branding.ts b/typescript/packages/cli/src/ui/branding.ts index 002b697..28adc99 100644 --- a/typescript/packages/cli/src/ui/branding.ts +++ b/typescript/packages/cli/src/ui/branding.ts @@ -5,6 +5,24 @@ import ora, { Ora } from 'ora'; // OFFICIAL MCP BRANDING (Wekan Enterprise Solutions) // ═══════════════════════════════════════════════════════════════════════════ +export const noColor = !!process.env.NO_COLOR; + +// ASCII fallback chars for box-drawing and emoji +const BOX = noColor + ? { TL: '+', TR: '+', BL: '+', BR: '+', TH: '=', TV: '|', tl: '+', tr: '+', bl: '+', br: '+', h: '-', v: '|' } + : { TL: '╔', TR: '╗', BL: '╚', BR: '╝', TH: '═', TV: '║', tl: '┌', tr: '┐', bl: '└', br: '┘', h: '─', v: '│' }; + +export const icons = { + ok: noColor ? '[ok]' : '✓', + err: noColor ? '[!!]' : '✗', + info: noColor ? '[i]' : 'ℹ', + warn: noColor ? '[!]' : '⚠', + dot: noColor ? '.' : '·', + bolt: noColor ? '!' : '⚡', + wave: noColor ? '' : ' 👋', + party: noColor ? '' : ' 🎉', +}; + // Core Colors const SIGNAL_BLUE = '#187CF4'; // Primary const SKY_BLUE = '#05A3FD'; // Secondary @@ -54,14 +72,22 @@ function boxLine(content: string, borderColor: (s: string) => string = brand.sig const paddingSize = Math.max(0, TOTAL_WIDTH - visualLength - 2); const padding = ' '.repeat(paddingSize); - return borderColor('║') + content + padding + borderColor('║'); + return borderColor(BOX.TV) + content + padding + borderColor(BOX.TV); } /** * Redesigned Banner with Restored ASCII NITRO */ -export const NITRO_BANNER_FULL = ` -${brand.signalBold('╔' + '═'.repeat(TOTAL_WIDTH - 2) + '╗')} +export const NITRO_BANNER_FULL = noColor + ? ` ++${'='.repeat(TOTAL_WIDTH - 2)}+ +|${' '.repeat(TOTAL_WIDTH - 2)}| +| NITROSTACK -- Official MCP Framework${' '.repeat(TOTAL_WIDTH - 44)}| +|${' '.repeat(TOTAL_WIDTH - 2)}| ++${'='.repeat(TOTAL_WIDTH - 2)}+ +` + : ` +${brand.signalBold(BOX.TL + BOX.TH.repeat(TOTAL_WIDTH - 2) + BOX.TR)} ${boxLine('')} ${boxLine(' ' + brand.signalBold('███╗ ██╗██╗████████╗██████╗ ██████╗ '))} ${boxLine(' ' + brand.signalBold('████╗ ██║██║╚══██╔══╝██╔══██╗██╔═══██╗'))} @@ -72,21 +98,21 @@ ${boxLine(' ' + chalk.dim('╚═╝ ╚═══╝╚═╝ ╚═╝ ${boxLine('')} ${boxLine(' ' + brand.signalBold('NITROSTACK') + ' ' + chalk.dim('─ Official MCP Framework'))} ${boxLine('')} -${brand.signalBold('╚' + '═'.repeat(TOTAL_WIDTH - 2) + '╝')} +${brand.signalBold(BOX.BL + BOX.TH.repeat(TOTAL_WIDTH - 2) + BOX.BR)} `; export function createHeader(title: string, subtitle?: string): string { const content = ' ' + brand.signalBold('NITROSTACK') + ' ' + chalk.dim('─') + ' ' + chalk.white.bold(title); const subContent = subtitle ? ' ' + chalk.dim(subtitle) : ''; - const borderTop = brand.signalBold('┌' + '─'.repeat(TOTAL_WIDTH - 2) + '┐'); - const borderBottom = brand.signalBold('└' + '─'.repeat(TOTAL_WIDTH - 2) + '┘'); + const borderTop = brand.signalBold(BOX.tl + BOX.h.repeat(TOTAL_WIDTH - 2) + BOX.tr); + const borderBottom = brand.signalBold(BOX.bl + BOX.h.repeat(TOTAL_WIDTH - 2) + BOX.br); const line = (c: string) => { const visualLength = stripAnsi(c).length; const paddingSize = Math.max(0, TOTAL_WIDTH - visualLength - 2); const padding = ' '.repeat(paddingSize); - return brand.signalBold('│') + c + padding + brand.signalBold('│'); + return brand.signalBold(BOX.v) + c + padding + brand.signalBold(BOX.v); }; let header = `\n${borderTop}\n${line(content)}\n`; @@ -100,15 +126,15 @@ export function createHeader(title: string, subtitle?: string): string { export function createBox(lines: string[], type: 'success' | 'error' | 'info' | 'warning' = 'info'): string { const colors = { - success: { border: brand.mint, bTop: '┌', bSide: '│', bBot: '└' }, - error: { border: brand.error, bTop: '┌', bSide: '│', bBot: '└' }, - info: { border: brand.signal, bTop: '┌', bSide: '│', bBot: '└' }, - warning: { border: brand.warning, bTop: '┌', bSide: '│', bBot: '└' }, + success: { border: brand.mint }, + error: { border: brand.error }, + info: { border: brand.signal }, + warning: { border: brand.warning }, }; - const { border, bTop, bSide, bBot } = colors[type]; + const { border } = colors[type]; - let output = border(bTop + '─'.repeat(TOTAL_WIDTH - 2) + '┐\n'); + let output = border(BOX.tl + BOX.h.repeat(TOTAL_WIDTH - 2) + BOX.tr + '\n'); for (let line of lines) { const maxInnerWidth = TOTAL_WIDTH - 6; @@ -120,17 +146,17 @@ export function createBox(lines: string[], type: 'success' | 'error' | 'info' | const finalVisualLength = stripAnsi(line).length; const padding = ' '.repeat(Math.max(0, TOTAL_WIDTH - finalVisualLength - 6)); - output += border(bSide) + ' ' + line + padding + ' ' + border(bSide) + '\n'; + output += border(BOX.v) + ' ' + line + padding + ' ' + border(BOX.v) + '\n'; } - output += border(bBot + '─'.repeat(TOTAL_WIDTH - 2) + '┘'); + output += border(BOX.bl + BOX.h.repeat(TOTAL_WIDTH - 2) + BOX.br); return output; } export function createSuccessBox(title: string, items: string[]): string { const lines = [ - brand.mintBold(`✓ ${title}`), + brand.mintBold(`${icons.ok} ${title}`), '', ...items.map(item => chalk.dim(` ${item}`)), '', @@ -140,7 +166,7 @@ export function createSuccessBox(title: string, items: string[]): string { export function createErrorBox(title: string, message: string): string { const lines = [ - brand.error.bold(`✗ ${title}`), + brand.error.bold(`${icons.err} ${title}`), '', chalk.white(message.substring(0, TOTAL_WIDTH - 10)), '', @@ -177,22 +203,22 @@ export class NitroSpinner { } succeed(text?: string): this { - this.spinner.succeed(text ? brand.mint('✓ ') + chalk.dim(text) : undefined); + this.spinner.succeed(text ? brand.mint(`${icons.ok} `) + chalk.dim(text) : undefined); return this; } fail(text?: string): this { - this.spinner.fail(text ? brand.error('✗ ') + chalk.dim(text) : undefined); + this.spinner.fail(text ? brand.error(`${icons.err} `) + chalk.dim(text) : undefined); return this; } info(text?: string): this { - this.spinner.info(text ? brand.signal('ℹ ') + chalk.dim(text) : undefined); + this.spinner.info(text ? brand.signal(`${icons.info} `) + chalk.dim(text) : undefined); return this; } warn(text?: string): this { - this.spinner.warn(text ? brand.warning('⚠ ') + chalk.dim(text) : undefined); + this.spinner.warn(text ? brand.warning(`${icons.warn} `) + chalk.dim(text) : undefined); return this; } @@ -203,19 +229,19 @@ export class NitroSpinner { } export function log(message: string, type: 'success' | 'error' | 'info' | 'warning' | 'dim' = 'info'): void { - const icons = { - success: brand.mint('✓'), - error: brand.error('✗'), - info: brand.signal('ℹ'), - warning: brand.warning('⚠'), - dim: chalk.dim('·'), + const logIcons = { + success: brand.mint(icons.ok), + error: brand.error(icons.err), + info: brand.signal(icons.info), + warning: brand.warning(icons.warn), + dim: chalk.dim(icons.dot), }; - console.log(` ${icons[type]} ${type === 'dim' ? chalk.dim(message) : message}`); + console.log(` ${logIcons[type]} ${type === 'dim' ? chalk.dim(message) : message}`); } export function divider(): void { - console.log(chalk.dim(' ' + '─'.repeat(TOTAL_WIDTH - 4))); + console.log(chalk.dim(' ' + BOX.h.repeat(TOTAL_WIDTH - 4))); } export function spacer(): void { From d1105df665fdb38e1c734bcf24d5057e07040b1b Mon Sep 17 00:00:00 2001 From: Manish Sharma Date: Thu, 25 Jun 2026 18:34:27 +0530 Subject: [PATCH 2/2] fix(cli): use ora stopAndPersist to avoid double symbol in NO_COLOR mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ora's succeed/fail/info/warn always prepend their own unicode symbol regardless of NO_COLOR, so prefixing icons.ok etc as text produced duplicate symbols (e.g. "✔ [ok] done"). Use stopAndPersist with an explicit symbol instead so only one symbol renders, ASCII or unicode. --- typescript/packages/cli/src/commands/dev.ts | 9 ++++----- typescript/packages/cli/src/ui/branding.ts | 8 ++++---- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/typescript/packages/cli/src/commands/dev.ts b/typescript/packages/cli/src/commands/dev.ts index 99a4c37..8ecdcfd 100644 --- a/typescript/packages/cli/src/commands/dev.ts +++ b/typescript/packages/cli/src/commands/dev.ts @@ -76,11 +76,10 @@ class DevUI { stopSpinner(success: boolean = true, text?: string): void { if (this.currentSpinner) { - if (success) { - this.currentSpinner.succeed(text); - } else { - this.currentSpinner.fail(text); - } + this.currentSpinner.stopAndPersist({ + symbol: success ? icons.ok : icons.err, + text, + }); this.currentSpinner = null; } } diff --git a/typescript/packages/cli/src/ui/branding.ts b/typescript/packages/cli/src/ui/branding.ts index 28adc99..8c5fcae 100644 --- a/typescript/packages/cli/src/ui/branding.ts +++ b/typescript/packages/cli/src/ui/branding.ts @@ -203,22 +203,22 @@ export class NitroSpinner { } succeed(text?: string): this { - this.spinner.succeed(text ? brand.mint(`${icons.ok} `) + chalk.dim(text) : undefined); + this.spinner.stopAndPersist({ symbol: brand.mint(icons.ok), text: text ? chalk.dim(text) : undefined }); return this; } fail(text?: string): this { - this.spinner.fail(text ? brand.error(`${icons.err} `) + chalk.dim(text) : undefined); + this.spinner.stopAndPersist({ symbol: brand.error(icons.err), text: text ? chalk.dim(text) : undefined }); return this; } info(text?: string): this { - this.spinner.info(text ? brand.signal(`${icons.info} `) + chalk.dim(text) : undefined); + this.spinner.stopAndPersist({ symbol: brand.signal(icons.info), text: text ? chalk.dim(text) : undefined }); return this; } warn(text?: string): this { - this.spinner.warn(text ? brand.warning(`${icons.warn} `) + chalk.dim(text) : undefined); + this.spinner.stopAndPersist({ symbol: brand.warning(icons.warn), text: text ? chalk.dim(text) : undefined }); return this; }