From db21b6d2888e2911c2732f818727564783961f3d Mon Sep 17 00:00:00 2001 From: Jonatan Bakucz Date: Tue, 4 Aug 2026 13:45:37 +0200 Subject: [PATCH 01/11] Add color theme setting with light and auto modes - Add workspace-scoped theme setting (dark/light/auto, default auto) - Ship dark + light palettes in src/tui/theme.ts; colors is now a mutable object so all views re-read the active palette - Auto mode probes the terminal via OSC 11 and falls back to COLORFGBG, then dark, if the terminal doesn't respond in 100ms - Wire the setting into the TUI config screen (hot-applies on save) and the CLI: 'leetcode config --theme ' plus the interactive prompt --- src/commands/config.ts | 32 +++++- src/index.ts | 2 + src/storage/config.ts | 13 ++- src/storage/workspaces.ts | 3 +- src/tui/commands/effects.ts | 9 ++ src/tui/index.ts | 7 ++ src/tui/screens/config/index.ts | 10 ++ src/tui/screens/config/view.ts | 8 +- src/tui/theme.ts | 171 +++++++++++++++++++++++++++++++- src/types.ts | 3 + 10 files changed, 253 insertions(+), 5 deletions(-) diff --git a/src/commands/config.ts b/src/commands/config.ts index 330ab49..a4be130 100644 --- a/src/commands/config.ts +++ b/src/commands/config.ts @@ -10,6 +10,7 @@ import { normalizeLeetCodeSiteInput, SUPPORTED_LEETCODE_SITES, } from '../utils/site.js'; +import { normalizeThemeInput, SUPPORTED_THEMES } from '../tui/theme.js'; interface ConfigOptions { lang?: string; @@ -17,12 +18,20 @@ interface ConfigOptions { workdir?: string; repo?: string | boolean; site?: string; + theme?: string; } export async function configCommand(options: ConfigOptions): Promise { const hasRepoOption = options.repo !== undefined; - if (!options.lang && !options.editor && !options.workdir && !hasRepoOption && !options.site) { + if ( + !options.lang && + !options.editor && + !options.workdir && + !hasRepoOption && + !options.site && + !options.theme + ) { await showCurrentConfig(); return; } @@ -96,12 +105,24 @@ export async function configCommand(options: ConfigOptions): Promise { config.setSite(normalizedSite); console.log(chalk.green(`βœ“ Site set to ${normalizedSite}`)); } + + if (options.theme) { + const normalizedTheme = normalizeThemeInput(options.theme); + if (!normalizedTheme) { + console.log(chalk.red(`Unsupported theme: ${options.theme}`)); + console.log(chalk.gray(`Supported: ${SUPPORTED_THEMES.join(', ')}`)); + return; + } + config.setTheme(normalizedTheme); + console.log(chalk.green(`βœ“ Theme set to ${normalizedTheme}`)); + } } export async function configInteractiveCommand(): Promise { const currentConfig = config.getConfig(); const workspace = config.getActiveWorkspace(); const currentSite = normalizeLeetCodeSiteInput(currentConfig.site ?? '') ?? DEFAULT_LEETCODE_SITE; + const currentTheme = currentConfig.theme ?? 'auto'; console.log(); console.log(chalk.bold.cyan(`πŸ“ Configuring workspace: ${workspace}`)); @@ -125,6 +146,13 @@ export async function configInteractiveCommand(): Promise { })), default: currentSite, }, + { + type: 'list', + name: 'theme', + message: 'Color theme:', + choices: SUPPORTED_THEMES.map((t) => ({ name: t, value: t })), + default: currentTheme, + }, { type: 'input', name: 'editor', @@ -148,6 +176,7 @@ export async function configInteractiveCommand(): Promise { config.setLanguage(answers.language); config.setEditor(answers.editor); config.setWorkDir(answers.workDir); + config.setTheme(answers.theme); if (answers.repo) { config.setRepo(answers.repo); } else { @@ -198,6 +227,7 @@ async function showCurrentConfig(): Promise { console.log(); console.log(chalk.gray('Language: '), chalk.white(currentConfig.language)); console.log(chalk.gray('Site: '), chalk.white(site)); + console.log(chalk.gray('Theme: '), chalk.white(currentConfig.theme ?? 'auto')); console.log(chalk.gray('Editor: '), chalk.white(currentConfig.editor ?? '(not set)')); console.log(chalk.gray('Work Dir: '), chalk.white(currentConfig.workDir)); console.log(chalk.gray('Repo URL: '), chalk.white(currentConfig.repo ?? '(not set)')); diff --git a/src/index.ts b/src/index.ts index c814df1..84a5c86 100644 --- a/src/index.ts +++ b/src/index.ts @@ -451,6 +451,7 @@ program .option('-e, --editor ', 'Set editor command') .option('-w, --workdir ', 'Set working directory for solutions') .option('-r, --repo [url]', 'Set Git repository URL (omit value to clear)') + .option('--theme ', 'Set color theme (dark, light, auto)') .option('-i, --interactive', 'Interactive configuration') .addHelpText( 'after', @@ -463,6 +464,7 @@ ${chalk.yellow('Examples:')} ${chalk.cyan('$ leetcode config -w ~/leetcode')} Set solutions folder ${chalk.cyan('$ leetcode config -r https://...')} Set git repository ${chalk.cyan('$ leetcode config --repo')} Clear git repository + ${chalk.cyan('$ leetcode config --theme light')} Use light color theme ${chalk.cyan('$ leetcode config -i')} Interactive setup ${chalk.gray(`Supported languages: ${getSupportedLanguagesLabel()}`)} diff --git a/src/storage/config.ts b/src/storage/config.ts index 2c22b1a..506978e 100644 --- a/src/storage/config.ts +++ b/src/storage/config.ts @@ -1,9 +1,11 @@ // Configuration management - delegates to workspace storage import { join } from 'path'; -import type { LeetCodeSite, SupportedLanguage, UserConfig } from '../types.js'; +import type { LeetCodeSite, SupportedLanguage, ThemeName, UserConfig } from '../types.js'; import { workspaceStorage } from './workspaces.js'; import { DEFAULT_LEETCODE_SITE, normalizeLeetCodeSiteInput } from '../utils/site.js'; +const DEFAULT_THEME: ThemeName = 'auto'; + export const config = { getConfig(): UserConfig { const wsConfig = workspaceStorage.getConfig(); @@ -13,6 +15,7 @@ export const config = { workDir: wsConfig.workDir, repo: wsConfig.syncRepo, site: normalizeLeetCodeSiteInput(wsConfig.site ?? '') ?? DEFAULT_LEETCODE_SITE, + theme: wsConfig.theme ?? DEFAULT_THEME, }; }, @@ -36,6 +39,10 @@ export const config = { workspaceStorage.setConfig({ site }); }, + setTheme(theme: ThemeName): void { + workspaceStorage.setConfig({ theme }); + }, + deleteRepo(): void { const wsConfig = workspaceStorage.getConfig(); delete wsConfig.syncRepo; @@ -64,6 +71,10 @@ export const config = { ); }, + getTheme(): ThemeName { + return workspaceStorage.getConfig().theme ?? DEFAULT_THEME; + }, + getPath(): string { return join(workspaceStorage.getWorkspaceDir(), 'config.json'); }, diff --git a/src/storage/workspaces.ts b/src/storage/workspaces.ts index 0247cff..6655c23 100644 --- a/src/storage/workspaces.ts +++ b/src/storage/workspaces.ts @@ -3,7 +3,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'; import { join } from 'path'; import { homedir } from 'os'; import { isValidWorkspaceName } from '../utils/validation.js'; -import type { LeetCodeSite } from '../types.js'; +import type { LeetCodeSite, ThemeName } from '../types.js'; export interface WorkspaceConfig { workDir: string; @@ -11,6 +11,7 @@ export interface WorkspaceConfig { editor?: string; syncRepo?: string; site?: LeetCodeSite; + theme?: ThemeName; } export interface WorkspaceRegistry { diff --git a/src/tui/commands/effects.ts b/src/tui/commands/effects.ts index 8434abe..d002a9f 100644 --- a/src/tui/commands/effects.ts +++ b/src/tui/commands/effects.ts @@ -18,6 +18,7 @@ import { requestExit } from '../runtime.js'; import got from 'got'; import { configureLeetCodeClientSite } from '../../utils/auth.js'; import { normalizeLeetCodeSiteInput } from '../../utils/site.js'; +import { applyTheme, normalizeThemeInput } from '../theme.js'; const RELEASES_URL = 'https://raw.githubusercontent.com/night-slayer18/leetcode-cli/main/docs/releases.md'; @@ -266,6 +267,14 @@ function saveConfig(key: string, value: string): void { case 'repo': config.setRepo(value); break; + case 'theme': { + const theme = normalizeThemeInput(value); + if (theme) { + config.setTheme(theme); + applyTheme(theme); + } + break; + } } } diff --git a/src/tui/index.ts b/src/tui/index.ts index 1d7c64d..3ae207d 100644 --- a/src/tui/index.ts +++ b/src/tui/index.ts @@ -1,5 +1,7 @@ import { runApp } from './runtime.js'; import { createInitialModel } from './types.js'; +import { applyTheme, primeAutoTheme } from './theme.js'; +import { config } from '../storage/config.js'; interface LaunchOptions { username?: string; @@ -7,6 +9,11 @@ interface LaunchOptions { export async function launchTUI(options: LaunchOptions = {}): Promise { const { username } = options; + const theme = config.getTheme(); + if (theme === 'auto') { + await primeAutoTheme(); + } + applyTheme(theme); const initialModel = createInitialModel(username); await runApp(initialModel); } diff --git a/src/tui/screens/config/index.ts b/src/tui/screens/config/index.ts index a0f886e..a1b6914 100644 --- a/src/tui/screens/config/index.ts +++ b/src/tui/screens/config/index.ts @@ -2,6 +2,7 @@ import type { ConfigScreenModel, ConfigMsg, Command } from '../../types.js'; import { Cmd } from '../../types.js'; import { config } from '../../../storage/config.js'; import { DEFAULT_LEETCODE_SITE, normalizeLeetCodeSiteInput } from '../../../utils/site.js'; +import { normalizeThemeInput, SUPPORTED_THEMES } from '../../theme.js'; type ConfigOption = ConfigScreenModel['options'][number]; @@ -19,6 +20,12 @@ function buildOptions(currentConfig: ReturnType): Confi description: 'Target site for API operations (leetcode.com or leetcode.cn)', value: currentConfig.site || DEFAULT_LEETCODE_SITE, }, + { + id: 'theme', + label: 'Color Theme', + description: `Color palette for the TUI (${SUPPORTED_THEMES.join(', ')})`, + value: currentConfig.theme ?? 'auto', + }, { id: 'editor', label: 'Editor Command', @@ -55,6 +62,9 @@ function validate(option: ConfigOption, value: string): string | null { if (option.id === 'site' && !normalizeLeetCodeSiteInput(trimmed)) { return 'Site must be leetcode.com or leetcode.cn'; } + if (option.id === 'theme' && !normalizeThemeInput(trimmed)) { + return `Theme must be one of: ${SUPPORTED_THEMES.join(', ')}`; + } return null; } diff --git a/src/tui/screens/config/view.ts b/src/tui/screens/config/view.ts index d1a1a08..ecf7d47 100644 --- a/src/tui/screens/config/view.ts +++ b/src/tui/screens/config/view.ts @@ -14,6 +14,7 @@ import { colors, borders, icons } from '../../theme.js'; const EXAMPLES: Record = { language: 'Example: typescript, python3, cpp, sql', site: 'Example: leetcode.com or leetcode.cn', + theme: 'Example: dark, light, auto', editor: 'Example: code, zed, vim, nvim', workdir: 'Example: /Users/name/leetcode', repo: 'Example: https://github.com/user/leetcode.git', @@ -105,7 +106,12 @@ function renderOptionDetails(model: ConfigScreenModel, width: number): string[] lines.push(chalk.hex(colors.textMuted)(truncate(EXAMPLES[option.id] || '', paneWidth - 2))); lines.push(''); - if (option.id === 'language' || option.id === 'workdir' || option.id === 'site') { + if ( + option.id === 'language' || + option.id === 'workdir' || + option.id === 'site' || + option.id === 'theme' + ) { lines.push(chalk.hex(colors.textMuted)('Validation: required')); } else { lines.push(chalk.hex(colors.textMuted)('Validation: optional')); diff --git a/src/tui/theme.ts b/src/tui/theme.ts index 2df81e0..ce8dc0d 100644 --- a/src/tui/theme.ts +++ b/src/tui/theme.ts @@ -1,4 +1,30 @@ -export const colors = { +export type ThemeName = 'dark' | 'light' | 'auto'; + +export const SUPPORTED_THEMES: readonly ThemeName[] = ['dark', 'light', 'auto']; + +export interface Palette { + primary: string; + primaryDark: string; + secondary: string; + success: string; + warning: string; + error: string; + info: string; + text: string; + textMuted: string; + textDim: string; + bg: string; + panel: string; + border: string; + borderFocus: string; + textBright: string; + bgHighlight: string; + cyan: string; + orange: string; + purple: string; +} + +export const darkPalette: Palette = { primary: '#00B8D4', primaryDark: '#006064', secondary: '#FF4081', @@ -20,6 +46,149 @@ export const colors = { purple: '#9C27B0', }; +export const lightPalette: Palette = { + primary: '#00838F', + primaryDark: '#004D5A', + secondary: '#C2185B', + success: '#1B7A2E', + warning: '#B26A00', + error: '#C62828', + info: '#1565C0', + text: '#1C1C1C', + textMuted: '#4A5860', + textDim: '#78909C', + bg: '#FFFFFF', + panel: '#F5F7F8', + border: '#B0BEC5', + borderFocus: '#00838F', + textBright: '#000000', + bgHighlight: '#E1ECF0', + cyan: '#00838F', + orange: '#E65100', + purple: '#6A1B9A', +}; + +/** + * The active palette. Consumers destructure `colors` at import time, so + * we keep the same object identity and mutate fields when the theme changes. + */ +export const colors: Palette = { ...darkPalette }; + +let cachedAutoTheme: 'dark' | 'light' | null = null; + +function parseOsc11(response: string): 'dark' | 'light' | null { + // Terminals reply with: \x1b]11;rgb:RRRR/GGGG/BBBB\x07 (or ST \x1b\\) + const match = response.match(/rgb:([0-9a-f]+)\/([0-9a-f]+)\/([0-9a-f]+)/i); + if (!match) return null; + + const scale = (h: string) => { + const n = Number.parseInt(h, 16); + if (!Number.isFinite(n)) return NaN; + // Normalize regardless of channel width (2, 4, ... hex digits). + return n / (16 ** h.length - 1); + }; + const r = scale(match[1]); + const g = scale(match[2]); + const b = scale(match[3]); + if (![r, g, b].every(Number.isFinite)) return null; + + // Perceived luminance (Rec. 709). + const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b; + return luminance > 0.5 ? 'light' : 'dark'; +} + +/** + * Ask the terminal for its background color via OSC 11. Times out quickly so + * unsupported terminals fall through to COLORFGBG / dark. + */ +export async function detectTerminalTheme(timeoutMs = 100): Promise<'dark' | 'light' | null> { + const { stdin, stdout } = process; + if (!stdin.isTTY || !stdout.isTTY) return null; + + return new Promise((resolve) => { + let buffer = ''; + let done = false; + const wasRaw = stdin.isRaw; + + const finish = (result: 'dark' | 'light' | null) => { + if (done) return; + done = true; + clearTimeout(timer); + stdin.removeListener('data', onData); + try { + stdin.setRawMode(wasRaw); + } catch { + // ignore + } + if (!wasRaw) stdin.pause(); + resolve(result); + }; + + const onData = (chunk: Buffer) => { + buffer += chunk.toString('utf8'); + const parsed = parseOsc11(buffer); + if (parsed) finish(parsed); + else if (buffer.length > 128) finish(null); + }; + + try { + stdin.setRawMode(true); + } catch { + resolve(null); + return; + } + stdin.resume(); + stdin.on('data', onData); + + const timer = setTimeout(() => finish(null), timeoutMs); + stdout.write('\x1b]11;?\x1b\\'); + }); +} + +function resolveFromEnv(): 'dark' | 'light' | null { + // COLORFGBG is set by rxvt-style terminals as "fg;bg" (e.g. "15;0" dark, "0;15" light). + const raw = process.env.COLORFGBG; + if (!raw) return null; + const parts = raw.split(';'); + const bg = parts[parts.length - 1]; + const n = Number.parseInt(bg, 10); + if (!Number.isFinite(n)) return null; + return n >= 7 && n <= 15 ? 'light' : 'dark'; +} + +/** + * Run terminal-theme detection once and cache it. Call this before + * `applyTheme('auto')` so the async probe can inform the palette choice. + */ +export async function primeAutoTheme(): Promise { + if (cachedAutoTheme) return; + const detected = await detectTerminalTheme(); + cachedAutoTheme = detected ?? resolveFromEnv() ?? 'dark'; +} + +function resolveAuto(): 'dark' | 'light' { + return cachedAutoTheme ?? resolveFromEnv() ?? 'dark'; +} + +export function resolveTheme(name: ThemeName | undefined): 'dark' | 'light' { + if (name === 'light') return 'light'; + if (name === 'dark') return 'dark'; + return resolveAuto(); +} + +export function applyTheme(name: ThemeName | undefined): void { + const resolved = resolveTheme(name); + const source = resolved === 'light' ? lightPalette : darkPalette; + Object.assign(colors, source); +} + +export function normalizeThemeInput(input: string): ThemeName | null { + const normalized = input.trim().toLowerCase(); + return (SUPPORTED_THEMES as readonly string[]).includes(normalized) + ? (normalized as ThemeName) + : null; +} + export const icons = { check: 'βœ”', cross: 'βœ–', diff --git a/src/types.ts b/src/types.ts index aa7be76..8698ac9 100644 --- a/src/types.ts +++ b/src/types.ts @@ -177,10 +177,13 @@ export type SupportedLanguage = export type LeetCodeSite = 'leetcode.com' | 'leetcode.cn'; +export type ThemeName = 'dark' | 'light' | 'auto'; + export interface UserConfig { language: SupportedLanguage; editor?: string; workDir: string; repo?: string; site?: LeetCodeSite; + theme?: ThemeName; } From c336b471ab29ef402687b5a51d9f3dd85f8d82a6 Mon Sep 17 00:00:00 2001 From: Jonatan Bakucz Date: Tue, 4 Aug 2026 13:50:58 +0200 Subject: [PATCH 02/11] Add theme tests and README documentation --- README.md | 9 ++++ src/__tests__/tui/theme.test.ts | 90 +++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 src/__tests__/tui/theme.test.ts diff --git a/README.md b/README.md index 2d7e41a..2c365af 100644 --- a/README.md +++ b/README.md @@ -427,8 +427,17 @@ leetcode config --lang sql leetcode config --editor code leetcode config --workdir ~/leetcode leetcode config --repo https://github.com/username/leetcode-solutions.git + +# Pick a color theme for the TUI (dark, light, or auto) +leetcode config --theme light +leetcode config --theme dark +leetcode config --theme auto ``` +The `--theme` setting is workspace-scoped. `auto` (the default) probes the +terminal for its background color via OSC 11 and falls back to `COLORFGBG`, +then to `dark`, if the terminal doesn't reply. + ## Folder Structure Solution files are automatically organized by difficulty and category: diff --git a/src/__tests__/tui/theme.test.ts b/src/__tests__/tui/theme.test.ts new file mode 100644 index 0000000..fa92344 --- /dev/null +++ b/src/__tests__/tui/theme.test.ts @@ -0,0 +1,90 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { + applyTheme, + colors, + darkPalette, + lightPalette, + normalizeThemeInput, + resolveTheme, + SUPPORTED_THEMES, +} from '../../tui/theme.js'; + +const ORIGINAL_COLORFGBG = process.env.COLORFGBG; + +describe('TUI theme', () => { + beforeEach(() => { + delete process.env.COLORFGBG; + applyTheme('dark'); + }); + + afterEach(() => { + if (ORIGINAL_COLORFGBG === undefined) { + delete process.env.COLORFGBG; + } else { + process.env.COLORFGBG = ORIGINAL_COLORFGBG; + } + applyTheme('dark'); + }); + + describe('normalizeThemeInput', () => { + it('accepts every supported theme name', () => { + for (const name of SUPPORTED_THEMES) { + expect(normalizeThemeInput(name)).toBe(name); + } + }); + + it('is case-insensitive and trims whitespace', () => { + expect(normalizeThemeInput(' LIGHT ')).toBe('light'); + expect(normalizeThemeInput('Dark')).toBe('dark'); + expect(normalizeThemeInput('AUTO')).toBe('auto'); + }); + + it('returns null for unsupported input', () => { + expect(normalizeThemeInput('solarized')).toBeNull(); + expect(normalizeThemeInput('')).toBeNull(); + }); + }); + + describe('resolveTheme', () => { + it('returns the explicit palette for dark/light', () => { + expect(resolveTheme('dark')).toBe('dark'); + expect(resolveTheme('light')).toBe('light'); + }); + + it('falls back to dark when auto has no signal', () => { + expect(resolveTheme('auto')).toBe('dark'); + expect(resolveTheme(undefined)).toBe('dark'); + }); + + it('detects light terminals via COLORFGBG background code >= 7', () => { + process.env.COLORFGBG = '0;15'; + expect(resolveTheme('auto')).toBe('light'); + }); + + it('detects dark terminals via COLORFGBG background code < 7', () => { + process.env.COLORFGBG = '15;0'; + expect(resolveTheme('auto')).toBe('dark'); + }); + }); + + describe('applyTheme', () => { + it('swaps the mutable colors palette to light', () => { + applyTheme('light'); + expect(colors.bg).toBe(lightPalette.bg); + expect(colors.text).toBe(lightPalette.text); + }); + + it('restores the dark palette', () => { + applyTheme('light'); + applyTheme('dark'); + expect(colors.bg).toBe(darkPalette.bg); + expect(colors.text).toBe(darkPalette.text); + }); + + it('keeps the same object identity so importers keep working', () => { + const before = colors; + applyTheme('light'); + expect(colors).toBe(before); + }); + }); +}); From f217c9afe851bb344a729bc3eab0754357e1f32a Mon Sep 17 00:00:00 2001 From: night-slayer18 Date: Fri, 7 Aug 2026 00:04:40 +0530 Subject: [PATCH 03/11] fix(ci): remove duplicate pull_request trigger from pr-check.yml Signed-off-by: night-slayer18 --- .github/workflows/pr-check.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 7c7bd90..a8fc604 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -1,9 +1,6 @@ name: PR Review on: - pull_request: - branches: [main, dev] - types: [opened, edited, synchronize, reopened] pull_request_target: branches: [main, dev] types: [opened, edited, synchronize, reopened] From e41a731e218b28f330674f5a11ca176bd4c1c566 Mon Sep 17 00:00:00 2001 From: night-slayer18 Date: Fri, 7 Aug 2026 00:09:02 +0530 Subject: [PATCH 04/11] fix(ci): add concurrency config with cancel-in-progress to PR review and CI workflows Signed-off-by: night-slayer18 --- .github/workflows/ci.yml | 4 ++++ .github/workflows/pr-check.yml | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cc4abcf..475ab25 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,10 @@ on: pull_request: branches: [main, dev] +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: build: runs-on: ${{ matrix.os }} diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index a8fc604..fac7890 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -5,6 +5,10 @@ on: branches: [main, dev] types: [opened, edited, synchronize, reopened] +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + permissions: pull-requests: write contents: read From 84071d1fa8f44b095c1a705d82f7100478a55f9a Mon Sep 17 00:00:00 2001 From: night-slayer18 Date: Fri, 7 Aug 2026 00:16:28 +0530 Subject: [PATCH 05/11] chore(ci): add workflow_dispatch to CI workflow Signed-off-by: night-slayer18 --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 475ab25..ccca353 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,7 @@ on: branches: [main, dev] pull_request: branches: [main, dev] + workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} From 214d967165fa0740b573d251f088b673e0b013de Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 7 Aug 2026 09:25:39 +0000 Subject: [PATCH 06/11] fix(deps): auto npm audit fix 20260807 --- package-lock.json | 42 ++++++------------------------------------ 1 file changed, 6 insertions(+), 36 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5cf15bf..9b7a5cb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1303,9 +1303,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1323,9 +1320,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1343,9 +1337,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1363,9 +1354,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1383,9 +1371,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1403,9 +1388,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2592,9 +2574,9 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz", - "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "dev": true, "license": "MIT", "dependencies": { @@ -3560,9 +3542,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz", - "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", + "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", "funding": [ { "type": "github", @@ -4310,9 +4292,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -4334,9 +4313,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -4358,9 +4334,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -4382,9 +4355,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ From bb2907db76033fcd6dd0ecf905db2d1281dd3488 Mon Sep 17 00:00:00 2001 From: night-slayer18 Date: Fri, 7 Aug 2026 15:00:39 +0530 Subject: [PATCH 07/11] ci(workflow): remove auto-merge logic from auto security audit workflow Signed-off-by: night-slayer18 --- .github/workflows/auto-audit.yml | 34 +++----------------------------- 1 file changed, 3 insertions(+), 31 deletions(-) diff --git a/.github/workflows/auto-audit.yml b/.github/workflows/auto-audit.yml index 68bee4c..208bbee 100644 --- a/.github/workflows/auto-audit.yml +++ b/.github/workflows/auto-audit.yml @@ -104,7 +104,7 @@ jobs: # failing the job (they require a manual semver-major update). npm audit || true - # ── Commit, push, PR to dev, then PR to main ────────────────────────── + # ── Commit, push, and open PR to dev ─────────────────────────────────── - name: Commit fix and open PR to dev if: steps.audit.outputs.found == 'true' && steps.changes.outputs.has_changes == 'true' env: @@ -118,7 +118,7 @@ jobs: git checkout -b "$BRANCH" git add package.json package-lock.json - git commit -m "fix(deps): auto npm audit fix ${DATE}" + git commit -s -m "fix(deps): auto npm audit fix ${DATE}" git push origin "$BRANCH" gh pr create \ @@ -135,32 +135,4 @@ jobs: - All checks passed: typecheck βœ… lint βœ… build βœ… tests βœ… ### Next steps - This PR is set to auto-merge into \`dev\` once all required checks pass. - A follow-up PR from \`dev β†’ main\` will be opened automatically after." - - # Auto-merge into dev when all checks pass - gh pr merge "$BRANCH" --merge --auto - - - name: Open PR from dev to main - if: steps.audit.outputs.found == 'true' && steps.changes.outputs.has_changes == 'true' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - DATE=$(date +%Y%m%d) - - # Wait for the dev merge to land before targeting main - sleep 30 - - gh pr create \ - --base main \ - --head dev \ - --title "fix(deps): security audit fix (${DATE})" \ - --body "## Security Audit Fix β†’ main - - Automatically opened after the audit fix was merged into \`dev\` and all CI checks passed. - - See the dev PR for full details." \ - || echo "::notice::PR devβ†’main already open or dev is already up to date with main" - - # Auto-merge into main when all checks pass - gh pr merge dev --merge --auto || true + Please review and merge this PR into \`dev\` manually." From ee4650874132eacd98c8b4d6b7243fdb475d5aee Mon Sep 17 00:00:00 2001 From: night-slayer18 Date: Fri, 14 Aug 2026 21:21:08 +0530 Subject: [PATCH 08/11] fix(api): allow null translatedName and tags on leetcode.cn (#24) Signed-off-by: night-slayer18 --- src/__tests__/api/cn-adapter.test.ts | 79 ++++++++++++++++++++++++++ src/api/adapters/cn.ts | 84 ++++++++++++++-------------- src/schemas/api.ts | 47 +++++++++------- 3 files changed, 148 insertions(+), 62 deletions(-) diff --git a/src/__tests__/api/cn-adapter.test.ts b/src/__tests__/api/cn-adapter.test.ts index 8d37bcd..5c971a9 100644 --- a/src/__tests__/api/cn-adapter.test.ts +++ b/src/__tests__/api/cn-adapter.test.ts @@ -151,6 +151,85 @@ describe('cn adapters', () => { expect(result.companyTags).toBeNull(); }); + it('handles null translatedName in topicTags without failing validation', () => { + const raw = { + question: { + questionId: '22', + questionFrontendId: '22', + title: 'Generate Parentheses', + translatedTitle: 'ζ‹¬ε·η”Ÿζˆ', + titleSlug: 'generate-parentheses', + translatedContent: '

ζ•°ε­— n δ»£θ‘¨η”Ÿζˆζ‹¬ε·ηš„ε―Ήζ•°...

', + difficulty: 'Medium', + isPaidOnly: false, + acRate: 0.78, + status: null, + topicTags: [ + { name: 'String', slug: 'string', translatedName: '字符串' }, + { name: 'Dynamic Programming', slug: 'dynamic-programming', translatedName: 'εŠ¨ζ€θ§„εˆ’' }, + { name: 'Backtracking', slug: 'backtracking', translatedName: 'ε›žζΊ―' }, + { name: 'SpecialTagWithoutTranslation', slug: 'special', translatedName: null }, + ], + codeSnippets: null, + sampleTestCase: '3', + exampleTestcases: '3\n1', + hints: null, + stats: null, + }, + }; + + const parsed = CnProblemDetailSchema.parse(raw); + const result = normalizeCnProblemDetail(parsed); + + expect(result.title).toBe('ζ‹¬ε·η”Ÿζˆ'); + expect(result.topicTags).toEqual([ + { name: '字符串', slug: 'string' }, + { name: 'εŠ¨ζ€θ§„εˆ’', slug: 'dynamic-programming' }, + { name: 'ε›žζΊ―', slug: 'backtracking' }, + { name: 'SpecialTagWithoutTranslation', slug: 'special' }, + ]); + expect(result.hints).toEqual([]); + expect(result.stats).toBe('{}'); + }); + + it('handles null nameTranslated in problem list and daily challenge schemas', () => { + const listParsed = CnProblemListSchema.parse({ + problemsetQuestionList: { + total: 1, + questions: [ + { + frontendQuestionId: '22', + title: 'Generate Parentheses', + titleCn: 'ζ‹¬ε·η”Ÿζˆ', + titleSlug: 'generate-parentheses', + difficulty: 'Medium', + paidOnly: false, + topicTags: [{ name: 'Backtracking', nameTranslated: null, slug: 'backtracking' }], + }, + ], + }, + }); + + const listResult = normalizeCnProblemList(listParsed); + expect(listResult.problems[0]?.topicTags).toEqual([{ name: 'Backtracking', slug: 'backtracking' }]); + + const dailyResult = normalizeCnDailyChallenge({ + todayRecord: [ + { + date: '2026-08-14', + question: { + questionId: '22', + title: 'Generate Parentheses', + titleCn: null, + topicTags: [{ name: 'Backtracking', nameTranslated: null, id: null }], + }, + }, + ], + }); + expect(dailyResult.question.title).toBe('Generate Parentheses'); + expect(dailyResult.question.topicTags).toEqual([{ name: 'Backtracking', slug: 'backtracking' }]); + }); + it('normalizes cn profile payload into shared user profile shape', () => { const profile = normalizeCnUserProfile('night-slayer', { userProfilePublicProfile: { diff --git a/src/api/adapters/cn.ts b/src/api/adapters/cn.ts index 78cab7c..fc459ad 100644 --- a/src/api/adapters/cn.ts +++ b/src/api/adapters/cn.ts @@ -1,62 +1,62 @@ import type { DailyChallenge, Problem, ProblemDetail } from '../../types.js'; interface CnTopicTag { - name?: string; - nameTranslated?: string; - id?: string | number; + name?: string | null; + nameTranslated?: string | null; + id?: string | number | null; } interface CnQuestion { - questionId?: string | number; - frontendQuestionId?: string | number; - questionFrontendId?: string | number; - difficulty?: string; - title?: string; - titleCn?: string; - titleSlug?: string; - paidOnly?: boolean; - isPaidOnly?: boolean; - acRate?: number | string; + questionId?: string | number | null; + frontendQuestionId?: string | number | null; + questionFrontendId?: string | number | null; + difficulty?: string | null; + title?: string | null; + titleCn?: string | null; + titleSlug?: string | null; + paidOnly?: boolean | null; + isPaidOnly?: boolean | null; + acRate?: number | string | null; status?: string | null; - topicTags?: CnTopicTag[]; + topicTags?: CnTopicTag[] | null; } interface CnProblemListItem { - frontendQuestionId?: string | number; - title?: string; - titleCn?: string; - titleSlug?: string; - difficulty?: string; - paidOnly?: boolean; - acRate?: number | string; + frontendQuestionId?: string | number | null; + title?: string | null; + titleCn?: string | null; + titleSlug?: string | null; + difficulty?: string | null; + paidOnly?: boolean | null; + acRate?: number | string | null; status?: string | null; - topicTags?: Array; + topicTags?: Array | null; } interface CnProblemDetailTag { - name?: string; - slug?: string; - translatedName?: string; + name?: string | null; + slug?: string | null; + translatedName?: string | null; } interface CnProblemDetailShape { question: { - questionId?: string | number; - questionFrontendId?: string | number; - title?: string; - translatedTitle?: string; - titleSlug?: string; + questionId?: string | number | null; + questionFrontendId?: string | number | null; + title?: string | null; + translatedTitle?: string | null; + titleSlug?: string | null; translatedContent?: string | null; - difficulty?: string; - isPaidOnly?: boolean; - acRate?: number | string; + difficulty?: string | null; + isPaidOnly?: boolean | null; + acRate?: number | string | null; status?: string | null; - topicTags?: CnProblemDetailTag[]; + topicTags?: CnProblemDetailTag[] | null; codeSnippets?: Array<{ lang: string; langSlug: string; code: string }> | null; - sampleTestCase?: string; - exampleTestcases?: string; - hints?: string[]; - stats?: string; + sampleTestCase?: string | null; + exampleTestcases?: string | null; + hints?: string[] | null; + stats?: string | null; }; } @@ -102,7 +102,7 @@ interface CnSkillShape { } | null; } -function toTitleCaseDifficulty(difficulty?: string): Problem['difficulty'] { +function toTitleCaseDifficulty(difficulty?: string | null): Problem['difficulty'] { const value = (difficulty ?? '').toLowerCase(); if (value === 'easy') return 'Easy'; if (value === 'hard') return 'Hard'; @@ -132,7 +132,7 @@ function toProblem(question: CnQuestion): Problem { const tagName = tag.nameTranslated || tag.name || 'Tag'; return { name: tagName, - slug: tag.id !== undefined ? String(tag.id) : toSlug(tagName), + slug: tag.id !== undefined && tag.id !== null ? String(tag.id) : toSlug(tagName), }; }); @@ -156,7 +156,9 @@ function toProblemFromListEntry(question: CnProblemListItem): Problem { const tagName = tag.nameTranslated || tag.name || 'Tag'; return { name: tagName, - slug: tag.slug || (tag.id !== undefined ? String(tag.id) : toSlug(tagName)), + slug: + tag.slug || + (tag.id !== undefined && tag.id !== null ? String(tag.id) : toSlug(tagName)), }; }); diff --git a/src/schemas/api.ts b/src/schemas/api.ts index 4e4cbc9..57b76cd 100644 --- a/src/schemas/api.ts +++ b/src/schemas/api.ts @@ -62,9 +62,9 @@ export const CnDailyChallengeSchema = z.object({ questionId: z.union([z.string(), z.number()]).optional(), frontendQuestionId: z.union([z.string(), z.number()]).optional(), questionFrontendId: z.union([z.string(), z.number()]).optional(), - difficulty: z.string().optional(), + difficulty: z.string().nullable().optional(), title: z.string().optional(), - titleCn: z.string().optional(), + titleCn: z.string().nullable().optional(), titleSlug: z.string().optional(), paidOnly: z.boolean().optional(), isPaidOnly: z.boolean().optional(), @@ -73,16 +73,19 @@ export const CnDailyChallengeSchema = z.object({ topicTags: z .array( z.object({ - name: z.string().optional(), - nameTranslated: z.string().optional(), - id: z.union([z.string(), z.number()]).optional(), + name: z.string().nullable().optional(), + nameTranslated: z.string().nullable().optional(), + id: z.union([z.string(), z.number()]).nullable().optional(), }) ) + .nullable() .optional(), }) + .nullable() .optional(), }) ) + .nullable() .optional(), }); @@ -93,21 +96,22 @@ export const CnProblemListSchema = z.object({ z.object({ frontendQuestionId: z.union([z.string(), z.number()]).optional(), title: z.string().optional(), - titleCn: z.string().optional(), + titleCn: z.string().nullable().optional(), titleSlug: z.string().optional(), - difficulty: z.string().optional(), + difficulty: z.string().nullable().optional(), paidOnly: z.boolean().optional(), acRate: z.union([z.number(), z.string()]).optional(), status: z.string().nullable().optional(), topicTags: z .array( z.object({ - name: z.string().optional(), - nameTranslated: z.string().optional(), - id: z.union([z.string(), z.number()]).optional(), - slug: z.string().optional(), + name: z.string().nullable().optional(), + nameTranslated: z.string().nullable().optional(), + id: z.union([z.string(), z.number()]).nullable().optional(), + slug: z.string().nullable().optional(), }) ) + .nullable() .optional(), }) ), @@ -119,21 +123,22 @@ export const CnProblemDetailSchema = z.object({ questionId: z.union([z.string(), z.number()]).optional(), questionFrontendId: z.union([z.string(), z.number()]).optional(), title: z.string().optional(), - translatedTitle: z.string().optional(), + translatedTitle: z.string().nullable().optional(), titleSlug: z.string().optional(), translatedContent: z.string().nullable().optional(), - difficulty: z.string().optional(), + difficulty: z.string().nullable().optional(), isPaidOnly: z.boolean().optional(), acRate: z.union([z.number(), z.string()]).optional(), status: z.string().nullable().optional(), topicTags: z .array( z.object({ - name: z.string().optional(), - slug: z.string().optional(), - translatedName: z.string().optional(), + name: z.string().nullable().optional(), + slug: z.string().nullable().optional(), + translatedName: z.string().nullable().optional(), }) ) + .nullable() .optional(), codeSnippets: z .array( @@ -145,12 +150,12 @@ export const CnProblemDetailSchema = z.object({ ) .nullable() .optional(), - sampleTestCase: z.string().optional(), - exampleTestcases: z.string().optional(), - hints: z.array(z.string()).optional(), - stats: z.string().optional(), + sampleTestCase: z.string().nullable().optional(), + exampleTestcases: z.string().nullable().optional(), + hints: z.array(z.string()).nullable().optional(), + stats: z.string().nullable().optional(), }), - }); +}); // --- Contest Schemas --- From 054701cbe372054e8daf5f948f93fc3529683066 Mon Sep 17 00:00:00 2001 From: night-slayer18 Date: Fri, 14 Aug 2026 21:21:58 +0530 Subject: [PATCH 09/11] docs: update documentation for v3.5.0 release Signed-off-by: night-slayer18 --- docs/commands.md | 9 ++++++++- docs/config.md | 10 ++++++++++ docs/releases.md | 41 +++++++++++++++++++++++++++++++++++++++++ docs/tui.md | 14 ++++++++++++++ 4 files changed, 73 insertions(+), 1 deletion(-) diff --git a/docs/commands.md b/docs/commands.md index a8bcff2..a9b1ce3 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -579,6 +579,7 @@ View or set configuration. - `-e, --editor ` - Set editor command - `-w, --workdir ` - Set working directory for solutions - `-r, --repo [url]` - Set or clear Git repository URL +- `-t, --theme ` - Set TUI color theme (`dark`, `light`, or `auto`) - `-i, --interactive` - Interactive configuration mode **Examples**: @@ -599,6 +600,12 @@ leetcode config --site leetcode.cn # Set editor leetcode config -e code leetcode config --editor vim +leetcode config --editor zed + +# Set TUI color theme +leetcode config --theme light +leetcode config --theme dark +leetcode config --theme auto # Set working directory leetcode config -w ~/leetcode @@ -613,7 +620,7 @@ leetcode config -i leetcode config --interactive # Set multiple options -leetcode config -l cpp -e code -w ~/leetcode +leetcode config -l cpp -e code -w ~/leetcode --theme auto ``` --- diff --git a/docs/config.md b/docs/config.md index b7ef1a8..9d8d184 100644 --- a/docs/config.md +++ b/docs/config.md @@ -82,8 +82,17 @@ leetcode config --editor zed # Set Git repository leetcode config -r https://github.com/myuser/leetcode-solutions.git + +# Set TUI color theme (dark, light, or auto) +leetcode config --theme light +leetcode config --theme dark +leetcode config --theme auto ``` +The `--theme` setting is workspace-scoped. `auto` (the default) probes the +terminal for its background color via OSC 11 and falls back to `COLORFGBG`, +then to `dark`, if the terminal doesn't reply. + ## Settings Config is stored per-workspace in `~/.leetcode/workspaces//config.json`. @@ -95,6 +104,7 @@ Config is stored per-workspace in `~/.leetcode/workspaces//config.json`. | `workDir` | Directory where solution files are saved | | `syncRepo` | Remote Git repository URL | | `site` | LeetCode site (`leetcode.com` or `leetcode.cn`) | +| `theme` | TUI color theme (`dark`, `light`, or `auto`) | ## Workspace-Aware Storage diff --git a/docs/releases.md b/docs/releases.md index 6e83674..6b88a0c 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -1,5 +1,46 @@ # Release Notes +## v3.5.0 + +> **Release Date**: 2026-08-14 +> **Focus**: TUI Color Themes (Light/Dark/Auto) + LeetCode China Tag Null Fix + CI/CD Workflow Overhaul + +### πŸš€ Features + +#### Color Theme Setting (`leetcode config --theme `) + +Added workspace-scoped color theme support for the Terminal UI (TUI) with dark, light, and auto modes. + +- **Modes**: + - `auto` (default): Automatically detects terminal background color by querying the terminal via OSC 11, falling back to `COLORFGBG` or dark mode if the terminal does not respond within 100ms. + - `dark`: High-contrast dark theme palette tailored for dark terminals. + - `light`: Clean, readable light theme palette optimized for light terminal backgrounds. +- Configurable via CLI flag: `leetcode config --theme ` +- Configurable interactively in the TUI **Config** screen with immediate live palette hot-reloading on save. + +```bash +# Pick a color theme for the TUI +leetcode config --theme light +leetcode config --theme dark +leetcode config --theme auto +``` + +### πŸ› Bug Fixes + +#### LeetCode China Null Tag Validation ([#24](https://github.com/night-slayer18/leetcode-cli/issues/24)) + +- Fixed an `API Response Validation Failed` error when running `show` or `pick` against `leetcode.cn` for problems containing tags without Chinese translations. +- Updated `CnProblemDetailSchema`, `CnProblemListSchema`, and `CnDailyChallengeSchema` Zod definitions to treat `translatedName`, `nameTranslated`, and associated optional metadata as nullable. +- Gracefully falls back to the English tag `name` when Chinese translations are `null`. + +### βš™οΈ CI/CD & Maintenance + +- **Concurrency Controls**: Added concurrency groups with `cancel-in-progress` to PR review and CI workflows to cancel redundant runs on push updates. +- **Manual CI Dispatch**: Added `workflow_dispatch` trigger to the main CI workflow. +- **Security Audit Cleanup**: Removed auto-merge logic from security audit automation workflows. + +--- + ## v3.4.0 > **Release Date**: 2026-07-29 diff --git a/docs/tui.md b/docs/tui.md index b02f6ca..ff65156 100644 --- a/docs/tui.md +++ b/docs/tui.md @@ -69,6 +69,20 @@ Both screens use buffered editing: - `Esc`: cancel current edit or go back. - `Tab` / `h` / `l`: switch pane focus. +## Color Themes + +The TUI supports dark and light themes, configured per workspace: + +- **`auto`** (default): Automatically detects terminal background color by querying via OSC 11, falling back to the `COLORFGBG` environment variable or dark theme if unresponded within 100ms. +- **`dark`**: Classic dark palette with vibrant accents. +- **`light`**: High-readability light theme tailored for light terminal backgrounds. + +Theme changes can be selected in the TUI **Config** screen (applies immediately on save) or via: + +```bash +leetcode config --theme +``` + ## Terminal Cleanup Guarantees When leaving TUI (quit or opening external editor), the app restores terminal state: From fbbabf9af949ab8c3d81d86da34558271386191c Mon Sep 17 00:00:00 2001 From: night-slayer18 Date: Fri, 14 Aug 2026 21:24:53 +0530 Subject: [PATCH 10/11] chore(release): bump version to 3.5.0 Signed-off-by: night-slayer18 --- package-lock.json | 10 +++++----- package.json | 2 +- src/index.ts | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9b7a5cb..945374b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@night-slayer18/leetcode-cli", - "version": "3.4.0", + "version": "3.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@night-slayer18/leetcode-cli", - "version": "3.4.0", + "version": "3.5.0", "license": "Apache-2.0", "dependencies": { "@supabase/supabase-js": "^2.90.1", @@ -4663,9 +4663,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.16", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", - "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "dev": true, "funding": [ { diff --git a/package.json b/package.json index c569fa0..8ab5076 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@night-slayer18/leetcode-cli", - "version": "3.4.0", + "version": "3.5.0", "description": "A modern LeetCode CLI built with TypeScript", "type": "module", "main": "dist/index.js", diff --git a/src/index.ts b/src/index.ts index 84a5c86..7f185d4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -71,7 +71,7 @@ program .name('leetcode') .usage('[command] [options]') .description(chalk.bold.cyan('πŸ”₯ A modern LeetCode CLI built with TypeScript')) - .version('3.4.0', '-v, --version', 'Output the version number') + .version('3.5.0', '-v, --version', 'Output the version number') .helpOption('-h, --help', 'Display help for command') .addHelpText( 'after', From 0318cf38d6233c1b5548941a278f1893a948a113 Mon Sep 17 00:00:00 2001 From: night-slayer18 Date: Fri, 14 Aug 2026 21:29:05 +0530 Subject: [PATCH 11/11] fix(types): update CnDailyRecord to allow nullable fields for typecheck Signed-off-by: night-slayer18 --- src/api/adapters/cn.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/api/adapters/cn.ts b/src/api/adapters/cn.ts index fc459ad..c222b62 100644 --- a/src/api/adapters/cn.ts +++ b/src/api/adapters/cn.ts @@ -61,9 +61,9 @@ interface CnProblemDetailShape { } interface CnDailyRecord { - date?: string; - link?: string; - question?: CnQuestion; + date?: string | null; + link?: string | null; + question?: CnQuestion | null; } interface CnAcceptedItem { @@ -176,7 +176,7 @@ function toProblemFromListEntry(question: CnProblemListItem): Problem { } export function normalizeCnDailyChallenge(input: { - todayRecord?: CnDailyRecord[]; + todayRecord?: CnDailyRecord[] | null; }): DailyChallenge { const record = input.todayRecord?.[0]; if (!record || !record.question) {