diff --git a/.changeset/named-color-violation-tui.md b/.changeset/named-color-violation-tui.md new file mode 100644 index 00000000..4e226e35 --- /dev/null +++ b/.changeset/named-color-violation-tui.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Replace chalk named color with theme-aware hex in session-directory warning. diff --git a/apps/kimi-code/src/cli/run-prompt.ts b/apps/kimi-code/src/cli/run-prompt.ts index e639aed0..e9f4505e 100644 --- a/apps/kimi-code/src/cli/run-prompt.ts +++ b/apps/kimi-code/src/cli/run-prompt.ts @@ -166,7 +166,7 @@ async function resolvePromptSession( } if (target.workDir !== workDir) { stderr.write( - `${chalk.yellow( + `${chalk.hex('#E8A838')( `Session "${opts.session}" was created under a different directory.\n` + ` cd "${target.workDir}" && kimi -r ${opts.session}`, )}\n\n`, diff --git a/apps/kimi-code/src/tui/kimi-tui.ts b/apps/kimi-code/src/tui/kimi-tui.ts index 68258aaa..910fd26c 100644 --- a/apps/kimi-code/src/tui/kimi-tui.ts +++ b/apps/kimi-code/src/tui/kimi-tui.ts @@ -467,7 +467,7 @@ export class KimiTUI { if (target.workDir !== workDir) { this.state.ui.stop(); process.stderr.write( - `${chalk.yellow( + `${chalk.hex(this.state.theme.colors.warning)( `Session "${startup.sessionFlag}" was created under a different directory.\n` + ` cd "${target.workDir}" && kimi -r ${startup.sessionFlag}`, )}\n\n`, diff --git a/apps/kimi-code/test/tui/chalk-named-color-guard.test.ts b/apps/kimi-code/test/tui/chalk-named-color-guard.test.ts new file mode 100644 index 00000000..e37a1187 --- /dev/null +++ b/apps/kimi-code/test/tui/chalk-named-color-guard.test.ts @@ -0,0 +1,74 @@ +import { readFileSync, readdirSync } from 'node:fs'; +import { join, relative } from 'node:path'; + +import { describe, expect, it } from 'vitest'; + +const SRC_ROOT = join(__dirname, '..', '..', 'src'); + +const NAMED_COLORS = [ + 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', + 'white', 'gray', 'grey', 'black', + 'blackBright', 'whiteBright', 'redBright', 'greenBright', + 'yellowBright', 'blueBright', 'magentaBright', 'cyanBright', +]; + +const CHALK_NAMED_PATTERN = new RegExp( + `chalk\\.(${NAMED_COLORS.join('|')})\\(`, +); + +function walk(dir: string, files: string[] = []): string[] { + try { + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const p = join(dir, entry.name); + if (entry.isDirectory()) { + walk(p, files); + } else if (entry.name.endsWith('.ts') && !entry.name.endsWith('.test.ts') && !entry.name.endsWith('.spec.ts')) { + files.push(p); + } + } + } catch { /* skip */ } + return files; +} + +describe('chalk named color guard', () => { + it('forbids chalk named colors in production source code', () => { + const offenders: { file: string; line: number; snippet: string }[] = []; + const files = walk(SRC_ROOT); + let inBlockComment = false; + for (const file of files) { + const content = readFileSync(file, 'utf8'); + const lines = content.split('\n'); + for (let i = 0; i < lines.length; i++) { + const line = lines[i] ?? ''; + const trimmed = line.trimStart(); + + if (inBlockComment) { + if (trimmed.includes('*/')) inBlockComment = false; + continue; + } + + if (trimmed.startsWith('/*')) { + if (!trimmed.includes('*/')) inBlockComment = true; + continue; + } + + if (trimmed.startsWith('//') || trimmed.startsWith('*')) continue; + + CHALK_NAMED_PATTERN.lastIndex = 0; + const m = CHALK_NAMED_PATTERN.exec(line); + if (m) { + offenders.push({ + file: relative(SRC_ROOT, file), + line: i + 1, + snippet: line.trim(), + }); + } + } + } + expect( + offenders, + `Found chalk named color usages. Use chalk.hex(colors.) or theme styles instead.\n` + + offenders.map((o) => ` ${o.file}:${String(o.line)} ${o.snippet}`).join('\n'), + ).toEqual([]); + }); +}); diff --git a/apps/kimi-code/test/tui/terminal-theme.test.ts b/apps/kimi-code/test/tui/terminal-theme.test.ts index 68a1818b..05537837 100644 --- a/apps/kimi-code/test/tui/terminal-theme.test.ts +++ b/apps/kimi-code/test/tui/terminal-theme.test.ts @@ -1,6 +1,8 @@ import { describe, expect, it, vi } from "vitest"; import type { TUIState } from "#/tui/kimi-tui"; +import { darkColors, lightColors, getColorPalette } from "#/tui/theme/colors"; +import { createThemeStyles } from "#/tui/theme/styles"; import { DISABLE_TERMINAL_THEME_REPORTING, ENABLE_TERMINAL_THEME_REPORTING, @@ -158,3 +160,38 @@ describe("terminal theme tracking", () => { expect(state.terminal.write).toHaveBeenCalledWith(DISABLE_TERMINAL_THEME_REPORTING); }); }); + +describe('ColorPalette warning token', () => { + it('has a defined warning color in both themes', () => { + expect(darkColors.warning).toBeTruthy(); + expect(lightColors.warning).toBeTruthy(); + expect(darkColors.warning).not.toBe(lightColors.warning); + }); + + it('resolves the correct palette by theme name', () => { + expect(getColorPalette('dark')).toBe(darkColors); + expect(getColorPalette('light')).toBe(lightColors); + }); +}); + +describe('ThemeStyles warning helper', () => { + it('wraps text and includes the input', () => { + const styles = createThemeStyles(darkColors); + const result = styles.warning('test'); + expect(result).toContain('test'); + }); + + it('is a function that returns a string', () => { + const darkStyles = createThemeStyles(darkColors); + expect(typeof darkStyles.warning).toBe('function'); + expect(typeof darkStyles.warning('hello')).toBe('string'); + }); + + it('creates independent style sets per palette', () => { + const darkStyles = createThemeStyles(darkColors); + const lightStyles = createThemeStyles(lightColors); + expect(darkStyles.colors.warning).toBe(darkColors.warning); + expect(lightStyles.colors.warning).toBe(lightColors.warning); + expect(darkStyles.colors.warning).not.toBe(lightStyles.colors.warning); + }); +});