From 3b0d378708993a80578199f122d0679172970cf4 Mon Sep 17 00:00:00 2001 From: night-slayer18 Date: Fri, 17 Jul 2026 23:49:59 +0530 Subject: [PATCH 01/14] fix(docs): remove -w flag from Docker examples The -w /root/leetcode flag was overriding the container's WORKDIR /app, causing Node to resolve the relative entrypoint dist/index.js against /root/leetcode instead of /app where the build artifacts actually live. Fixes #18 Signed-off-by: night-slayer18 --- README.md | 4 ---- docs/docker.md | 5 ----- 2 files changed, 9 deletions(-) diff --git a/README.md b/README.md index 7eaaa79..30d479a 100644 --- a/README.md +++ b/README.md @@ -500,7 +500,6 @@ You can run the CLI using Docker without installing Node.js. ```bash leetcode() { docker run -it --rm \ - -w /root/leetcode \ -v "$(pwd)/leetcode:/root/leetcode" \ -v "$HOME/.leetcode:/root/.leetcode" \ nightslayer/leetcode-cli:latest "$@" @@ -512,7 +511,6 @@ You can run the CLI using Docker without installing Node.js. ```fish function leetcode docker run -it --rm \ - -w /root/leetcode \ -v (pwd)/leetcode:/root/leetcode \ -v $HOME/.leetcode:/root/.leetcode \ nightslayer/leetcode-cli:latest $argv @@ -524,7 +522,6 @@ You can run the CLI using Docker without installing Node.js. ```powershell function leetcode { docker run -it --rm ` - -w /root/leetcode ` -v "${PWD}/leetcode:/root/leetcode" ` -v "$env:USERPROFILE/.leetcode:/root/.leetcode" ` nightslayer/leetcode-cli:latest $args @@ -548,7 +545,6 @@ You can run the CLI using Docker without installing Node.js. 2. **Run commands**: ```bash docker run -it --rm \ - -w /root/leetcode \ -v "$(pwd)/leetcode:/root/leetcode" \ -v "$HOME/.leetcode:/root/.leetcode" \ leetcode-cli list diff --git a/docs/docker.md b/docs/docker.md index e981f86..e96e694 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -19,7 +19,6 @@ Add to your shell config (functions forward arguments properly, aliases don't): ```bash leetcode() { docker run -it --rm \ - -w /root/leetcode \ -v "$(pwd)/leetcode:/root/leetcode" \ -v "$HOME/.leetcode:/root/.leetcode" \ nightslayer/leetcode-cli:latest "$@" @@ -31,7 +30,6 @@ leetcode() { ```fish function leetcode docker run -it --rm \ - -w /root/leetcode \ -v (pwd)/leetcode:/root/leetcode \ -v $HOME/.leetcode:/root/.leetcode \ nightslayer/leetcode-cli:latest $argv @@ -43,7 +41,6 @@ end ```powershell function leetcode { docker run -it --rm ` - -w /root/leetcode ` -v "${PWD}/leetcode:/root/leetcode" ` -v "$env:USERPROFILE/.leetcode:/root/.leetcode" ` nightslayer/leetcode-cli:latest $args @@ -70,7 +67,6 @@ Use env credentials instead: docker run -it --rm \ -e LEETCODE_SESSION=\"\" \ -e LEETCODE_CSRF_TOKEN=\"\" \ - -w /root/leetcode \ -v \"$(pwd)/leetcode:/root/leetcode\" \ nightslayer/leetcode-cli:latest list ``` @@ -90,7 +86,6 @@ If you prefer to build it yourself: 2. **Run** (Bash/Zsh): ```bash docker run -it --rm \ - -w /root/leetcode \ -v "$(pwd)/leetcode:/root/leetcode" \ -v "$HOME/.leetcode:/root/.leetcode" \ leetcode-cli list From cbf786440de49b1807f8870804a696c2deb0ed7f Mon Sep 17 00:00:00 2001 From: nullptr Date: Mon, 20 Jul 2026 22:34:26 -0400 Subject: [PATCH 02/14] feat(reset): add solution reset command --- README.md | 14 +++ docs/commands.md | 22 ++++ src/__tests__/commands/reset.test.ts | 147 +++++++++++++++++++++++++++ src/commands/reset.ts | 93 +++++++++++++++++ src/index.ts | 19 ++++ 5 files changed, 295 insertions(+) create mode 100644 src/__tests__/commands/reset.test.ts create mode 100644 src/commands/reset.ts diff --git a/README.md b/README.md index 30d479a..3431881 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ A modern, feature-rich LeetCode CLI built with TypeScript. - ๐Ÿ“‹ **List problems** - Filter by difficulty, status, tags, and search - ๐Ÿ“– **Beautiful problem display** - Formatted output with examples and constraints - ๐Ÿ“ **Generate solution files** - Auto-organized by difficulty and category +- โ™ป๏ธ **Reset solution files** - Restore an existing local solution to the original stub - ๐Ÿงช **Test solutions** - Run against sample test cases - ๐Ÿ“ค **Submit solutions** - Submit directly to LeetCode - ๐Ÿ“Š **View statistics** - Track your progress @@ -63,6 +64,9 @@ leetcode daily # Pick a problem and generate solution file leetcode pick 1 +# Reset an existing solution back to the original stub +leetcode reset 1 + # Test your solution (any format works!) leetcode test 1 # Problem ID leetcode test 1.two-sum.java # Filename @@ -108,6 +112,7 @@ The CLI keeps command semantics the same and applies site-specific GraphQL queri | `hint ` | Show hints for a problem | | `pick ` | Generate solution file | | `pick-batch ` | Pick multiple problems | +| `reset ` | Reset solution file to original stub | | `bookmark ` | Manage problem bookmarks | | `note ` | Manage problem notes | | `daily` | Show today's challenge | @@ -178,6 +183,15 @@ leetcode pick 175 --lang sql leetcode pick 1 --no-open ``` +### Reset Problem + +Reset an existing local solution file back to the original LeetCode stub. This overwrites the file immediately. + +```bash +leetcode reset 1 +leetcode reset two-sum +``` + ### Test & Submit All formats work for both `test` and `submit`: diff --git a/docs/commands.md b/docs/commands.md index 6851209..b32e301 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -263,6 +263,28 @@ leetcode pick 1 -l cpp --no-open --- +### `leetcode reset ` + +Reset an existing local solution file back to the original LeetCode stub. + +This command overwrites the existing file immediately. It preserves the existing file path and language by detecting the file extension. + +**Arguments**: + +- `` - Problem ID or slug + +**Examples**: + +```bash +# By problem ID +leetcode reset 1 + +# By slug +leetcode reset two-sum +``` + +--- + ### `leetcode test ` (alias: `t`) Test solution against sample test cases. diff --git a/src/__tests__/commands/reset.test.ts b/src/__tests__/commands/reset.test.ts new file mode 100644 index 0000000..94055ad --- /dev/null +++ b/src/__tests__/commands/reset.test.ts @@ -0,0 +1,147 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { outputContains } from '../setup.js'; + +const { mockProblem } = vi.hoisted(() => ({ + mockProblem: { + questionId: '1', + questionFrontendId: '1', + title: 'Two Sum', + titleSlug: 'two-sum', + difficulty: 'Easy' as const, + content: '

Given an array...

', + topicTags: [{ name: 'Array', slug: 'array' }], + codeSnippets: [ + { + lang: 'TypeScript', + langSlug: 'typescript', + code: 'function twoSum(nums: number[], target: number): number[] {\n\n}', + }, + { + lang: 'Java', + langSlug: 'java', + code: 'class Solution {\n public int[] twoSum(int[] nums, int target) {\n\n }\n}', + }, + ], + exampleTestcases: '[2,7,11,15]\n9', + sampleTestCase: '[2,7,11,15]\n9', + hints: [], + companyTags: [], + stats: '{}', + isPaidOnly: false, + acRate: 0, + status: null, + }, +})); + +vi.mock('../../storage/credentials.js', () => ({ + credentials: { + get: vi.fn(() => ({ session: 'test', csrfToken: 'test' })), + status: vi.fn(), + }, +})); + +vi.mock('../../storage/config.js', () => ({ + config: { + getConfig: vi.fn(() => ({ + language: 'typescript', + workDir: '/tmp/leetcode', + site: 'leetcode.com', + })), + getWorkDir: vi.fn(() => '/tmp/leetcode'), + getSite: vi.fn(() => 'leetcode.com'), + }, +})); + +vi.mock('../../api/client.js', () => ({ + leetcodeClient: { + setSite: vi.fn(), + setCredentials: vi.fn(), + checkAuth: vi.fn().mockResolvedValue({ isSignedIn: true, username: 'TestUser' }), + getProblemById: vi.fn().mockResolvedValue(mockProblem), + getProblem: vi.fn().mockResolvedValue(mockProblem), + }, +})); + +vi.mock('../../utils/fileUtils.js', () => ({ + findSolutionFile: vi.fn().mockResolvedValue('/tmp/leetcode/Easy/Array/1.two-sum.ts'), + detectLanguageFromFile: vi.fn().mockReturnValue('typescript'), +})); + +vi.mock('fs/promises', () => ({ + writeFile: vi.fn().mockResolvedValue(undefined), +})); + +vi.mock('ora', () => ({ + default: vi.fn(() => ({ + start: vi.fn().mockReturnThis(), + stop: vi.fn().mockReturnThis(), + succeed: vi.fn().mockReturnThis(), + fail: vi.fn().mockReturnThis(), + warn: vi.fn().mockReturnThis(), + text: '', + })), +})); + +import { resetCommand } from '../../commands/reset.js'; +import { leetcodeClient } from '../../api/client.js'; +import { findSolutionFile, detectLanguageFromFile } from '../../utils/fileUtils.js'; +import { writeFile } from 'fs/promises'; + +describe('resetCommand', () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(leetcodeClient.getProblemById).mockResolvedValue(mockProblem); + vi.mocked(leetcodeClient.getProblem).mockResolvedValue(mockProblem); + vi.mocked(findSolutionFile).mockResolvedValue('/tmp/leetcode/Easy/Array/1.two-sum.ts'); + vi.mocked(detectLanguageFromFile).mockReturnValue('typescript'); + }); + + it('overwrites an existing solution file with the original stub', async () => { + const result = await resetCommand('1'); + + expect(result).toBe(true); + expect(leetcodeClient.getProblemById).toHaveBeenCalledWith('1'); + expect(findSolutionFile).toHaveBeenCalledWith('/tmp/leetcode', '1'); + expect(writeFile).toHaveBeenCalledWith( + '/tmp/leetcode/Easy/Array/1.two-sum.ts', + expect.stringContaining('function twoSum(nums: number[], target: number): number[]'), + 'utf-8' + ); + }); + + it('supports problem slugs', async () => { + await resetCommand('two-sum'); + + expect(leetcodeClient.getProblem).toHaveBeenCalledWith('two-sum'); + expect(writeFile).toHaveBeenCalled(); + }); + + it('does not write when no existing solution file is found', async () => { + vi.mocked(findSolutionFile).mockResolvedValueOnce(null); + + const result = await resetCommand('1'); + + expect(result).toBe(false); + expect(writeFile).not.toHaveBeenCalled(); + expect(outputContains('Run "leetcode pick 1" first')).toBe(true); + }); + + it('does not write when the existing file language is unsupported', async () => { + vi.mocked(detectLanguageFromFile).mockReturnValueOnce(null); + + const result = await resetCommand('1'); + + expect(result).toBe(false); + expect(writeFile).not.toHaveBeenCalled(); + }); + + it('does not write when no matching template is available', async () => { + vi.mocked(detectLanguageFromFile).mockReturnValueOnce('python3'); + + const result = await resetCommand('1'); + + expect(result).toBe(false); + expect(writeFile).not.toHaveBeenCalled(); + expect(outputContains('Available languages')).toBe(true); + }); +}); diff --git a/src/commands/reset.ts b/src/commands/reset.ts new file mode 100644 index 0000000..cd79a8f --- /dev/null +++ b/src/commands/reset.ts @@ -0,0 +1,93 @@ +// Reset command - restore an existing solution file to the original LeetCode stub +import { writeFile } from 'fs/promises'; +import { basename } from 'path'; +import ora from 'ora'; +import chalk from 'chalk'; +import { leetcodeClient } from '../api/client.js'; +import { requireAuth } from '../utils/auth.js'; +import { config } from '../storage/config.js'; +import { findSolutionFile, detectLanguageFromFile } from '../utils/fileUtils.js'; +import { generateSolutionFile, getPremiumPlaceholderCode } from '../utils/templates.js'; +import { isPathInsideWorkDir } from '../utils/validation.js'; +import { resolveSupportedLanguageFromLeetCodeSlug } from '../utils/languages.js'; + +export async function resetCommand(idOrSlug: string): Promise { + const { authorized } = await requireAuth(); + if (!authorized) return false; + + const spinner = ora({ text: 'Fetching problem details...', spinner: 'dots' }).start(); + + try { + const problem = /^\d+$/.test(idOrSlug) + ? await leetcodeClient.getProblemById(idOrSlug) + : await leetcodeClient.getProblem(idOrSlug); + + const workDir = config.getWorkDir(); + const filePath = await findSolutionFile(workDir, problem.questionFrontendId); + + if (!filePath) { + spinner.fail(`No solution file found for problem ${problem.questionFrontendId}`); + console.log(chalk.gray(`Looking in: ${workDir}`)); + console.log( + chalk.gray( + `Run "leetcode pick ${problem.questionFrontendId}" first to create a solution file.` + ) + ); + return false; + } + + if (!isPathInsideWorkDir(filePath, workDir)) { + spinner.fail('Security Error: File path is outside the configured workspace'); + console.log(chalk.gray(`File: ${filePath}`)); + console.log(chalk.gray(`Workspace: ${workDir}`)); + return false; + } + + const language = detectLanguageFromFile(filePath); + if (!language) { + spinner.fail(`Unsupported file extension: ${basename(filePath)}`); + return false; + } + + spinner.text = 'Generating solution stub...'; + + const snippets = problem.codeSnippets ?? []; + const template = + snippets.find( + (snippet) => resolveSupportedLanguageFromLeetCodeSlug(snippet.langSlug) === language + ) ?? null; + + let code: string; + if (snippets.length === 0) { + code = getPremiumPlaceholderCode(language, problem.title); + } else if (!template) { + spinner.fail(`No code template available for ${language}`); + console.log(chalk.gray(`Available languages: ${snippets.map((s) => s.langSlug).join(', ')}`)); + return false; + } else { + code = template.code; + } + + const content = generateSolutionFile( + problem.questionFrontendId, + problem.titleSlug, + problem.title, + problem.difficulty, + code, + language, + problem.content ?? undefined + ); + + await writeFile(filePath, content, 'utf-8'); + + spinner.succeed(`Reset ${chalk.green(basename(filePath))}`); + console.log(chalk.gray(`Path: ${filePath}`)); + return true; + } catch (error) { + spinner.fail('Failed to reset solution'); + if (error instanceof Error) { + console.log(chalk.red(error.message)); + } + return false; + } +} diff --git a/src/index.ts b/src/index.ts index 038111a..b4ed91e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,6 +9,7 @@ import { hintCommand } from './commands/hint.js'; import { pickCommand, batchPickCommand } from './commands/pick.js'; import { testCommand } from './commands/test.js'; import { submitCommand } from './commands/submit.js'; +import { resetCommand } from './commands/reset.js'; import { statCommand } from './commands/stat.js'; import { dailyCommand } from './commands/daily.js'; import { randomCommand } from './commands/random.js'; @@ -80,6 +81,7 @@ ${chalk.yellow('Examples:')} ${chalk.cyan('$ leetcode random -d medium')} Get random medium problem ${chalk.cyan('$ leetcode pick 1')} Start solving "Two Sum" ${chalk.cyan('$ leetcode test 1')} Test your solution + ${chalk.cyan('$ leetcode reset 1')} Reset solution to original stub ${chalk.cyan('$ leetcode submit 1')} Submit your solution ` ); @@ -261,6 +263,23 @@ ${chalk.gray('Testcases use \\n to separate multiple inputs.')} ) .action(testCommand); +program + .command('reset ') + .description('Reset solution file to the original LeetCode stub') + .addHelpText( + 'after', + ` +${chalk.yellow('Examples:')} + ${chalk.cyan('$ leetcode reset 1')} Reset problem 1 solution + ${chalk.cyan('$ leetcode reset two-sum')} Reset by problem slug + +${chalk.gray('Overwrites the existing local solution file immediately.')} +` + ) + .action(async (id) => { + await resetCommand(id); + }); + program .command('submit ') .alias('x') From 5ef0da837cbdd9e69999d1865a3aea910dc768ef Mon Sep 17 00:00:00 2001 From: Anurag Deo Date: Sun, 26 Jul 2026 13:29:07 +0530 Subject: [PATCH 03/14] feat: add contest models and validation Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/__tests__/api/client-contest.test.ts | 221 +++++++++++++++++++++++ src/schemas/api.ts | 49 +++++ src/types.ts | 23 +++ 3 files changed, 293 insertions(+) create mode 100644 src/__tests__/api/client-contest.test.ts diff --git a/src/__tests__/api/client-contest.test.ts b/src/__tests__/api/client-contest.test.ts new file mode 100644 index 0000000..7a04cab --- /dev/null +++ b/src/__tests__/api/client-contest.test.ts @@ -0,0 +1,221 @@ +import { describe, expect, it, vi } from 'vitest'; +import { LeetCodeClient } from '../../api/client.js'; + +type GraphqlMethod = ( + operation: string, + query: string, + variables?: Record +) => Promise; + +function mockGraphql(client: LeetCodeClient) { + return vi.spyOn(client as unknown as { graphql: GraphqlMethod }, 'graphql'); +} + +describe('LeetCodeClient contests', () => { + it('returns contests in API order', async () => { + const client = new LeetCodeClient(); + const graphql = mockGraphql(client); + graphql.mockResolvedValueOnce({ + allContests: [ + { + title: 'Later Contest', + titleSlug: 'later-contest', + startTime: 2, + duration: 5400, + originStartTime: 2, + isVirtual: false, + containsPremium: false, + }, + { + title: 'Earlier Contest', + titleSlug: 'earlier-contest', + startTime: 1, + duration: 5400, + originStartTime: 1, + isVirtual: false, + containsPremium: true, + }, + ], + }); + + const contests = await client.getContests(); + + expect(contests.map((contest) => contest.titleSlug)).toEqual([ + 'later-contest', + 'earlier-contest', + ]); + expect(graphql).toHaveBeenCalledWith('CONTEST_LIST', expect.any(String)); + }); + + it('returns ordered contest questions without requiring full Problem fields', async () => { + const client = new LeetCodeClient(); + const graphql = mockGraphql(client); + graphql.mockResolvedValueOnce({ + contest: { + title: 'Weekly Contest', + titleSlug: 'weekly-contest', + startTime: 1, + duration: 5400, + originStartTime: null, + isVirtual: false, + containsPremium: false, + description: null, + questions: [ + { + questionId: '2', + title: 'Second', + titleSlug: 'second', + }, + { + questionId: '1', + title: 'First', + titleSlug: 'first', + }, + ], + }, + }); + + const contest = await client.getContest('weekly-contest'); + + expect(contest.questions.map((question) => question.titleSlug)).toEqual(['second', 'first']); + expect(graphql).toHaveBeenCalledWith('CONTEST_DETAIL', expect.any(String), { + titleSlug: 'weekly-contest', + }); + }); + + it('throws a descriptive error when the contest detail is null', async () => { + const client = new LeetCodeClient(); + const graphql = mockGraphql(client); + graphql.mockResolvedValueOnce({ contest: null }); + + await expect(client.getContest('missing-contest')).rejects.toThrow( + 'Contest "missing-contest" not found' + ); + }); + + it('normalizes CN contest history in API order', async () => { + const client = new LeetCodeClient('leetcode.cn'); + const graphql = mockGraphql(client); + graphql.mockResolvedValueOnce({ + contestHistory: { + totalNum: 2, + contests: [ + { + containsPremium: false, + title: 'Later Contest', + titleSlug: 'later-contest', + description: 'Later', + startTime: 2, + duration: 5400, + originStartTime: 2, + isVirtual: false, + }, + { + containsPremium: true, + title: 'Earlier Contest', + titleSlug: 'earlier-contest', + description: null, + startTime: 1, + duration: 5400, + originStartTime: 1, + isVirtual: false, + }, + ], + }, + }); + + await expect(client.getContests()).resolves.toEqual([ + { + title: 'Later Contest', + titleSlug: 'later-contest', + startTime: 2, + duration: 5400, + originStartTime: 2, + isVirtual: false, + containsPremium: false, + }, + { + title: 'Earlier Contest', + titleSlug: 'earlier-contest', + startTime: 1, + duration: 5400, + originStartTime: 1, + isVirtual: false, + containsPremium: true, + }, + ]); + expect(graphql).toHaveBeenCalledWith('CONTEST_LIST', expect.stringContaining('contestHistory'), { + pageNum: 1, + pageSize: 100, + }); + }); + + it('fetches additional CN contest history pages in API order', async () => { + const client = new LeetCodeClient('leetcode.cn'); + const graphql = mockGraphql(client); + graphql + .mockResolvedValueOnce({ + contestHistory: { + totalNum: 2, + contests: [ + { + containsPremium: false, + title: 'First Contest', + titleSlug: 'first-contest', + description: null, + startTime: 1, + duration: 5400, + originStartTime: 1, + isVirtual: false, + }, + ], + }, + }) + .mockResolvedValueOnce({ + contestHistory: { + totalNum: 2, + contests: [ + { + containsPremium: true, + title: 'Second Contest', + titleSlug: 'second-contest', + description: null, + startTime: 2, + duration: 5400, + originStartTime: 2, + isVirtual: false, + }, + ], + }, + }); + + const contests = await client.getContests(); + + expect(contests.map((contest) => contest.titleSlug)).toEqual([ + 'first-contest', + 'second-contest', + ]); + expect(graphql).toHaveBeenNthCalledWith( + 2, + 'CONTEST_LIST', + expect.stringContaining('contestHistory'), + { pageNum: 2, pageSize: 100 } + ); + }); + + it('fails clearly when a contest list response is malformed', async () => { + const client = new LeetCodeClient(); + const graphql = mockGraphql(client); + graphql.mockResolvedValueOnce({}); + + await expect(client.getContests()).rejects.toThrow(/allContests/); + }); + + it('fails clearly when a CN contest history response is malformed', async () => { + const client = new LeetCodeClient('leetcode.cn'); + const graphql = mockGraphql(client); + graphql.mockResolvedValueOnce({ contestHistory: {} }); + + await expect(client.getContests()).rejects.toThrow(/totalNum|contests/); + }); +}); diff --git a/src/schemas/api.ts b/src/schemas/api.ts index aee6f50..4e4cbc9 100644 --- a/src/schemas/api.ts +++ b/src/schemas/api.ts @@ -150,6 +150,49 @@ export const CnProblemDetailSchema = z.object({ hints: z.array(z.string()).optional(), stats: z.string().optional(), }), + }); + +// --- Contest Schemas --- + +export const ContestSchema = z.object({ + title: z.string(), + titleSlug: z.string(), + startTime: z.number(), + duration: z.number(), + originStartTime: z.number().nullable().optional(), + isVirtual: z.boolean().nullable().optional(), + containsPremium: z.boolean().nullable().optional(), +}); + +export const ContestQuestionSchema = z.object({ + questionId: z.union([z.string(), z.number()]).transform(String), + title: z.string(), + titleSlug: z.string(), + difficulty: z.enum(['Easy', 'Medium', 'Hard']).nullable().optional(), +}); + +export const ContestDetailSchema = ContestSchema.extend({ + description: z.string().nullable().optional(), + questions: z.array(ContestQuestionSchema), +}); + +export const ContestListSchema = z.object({ + allContests: z.array(ContestSchema), +}); + +export const CnContestHistorySchema = z.object({ + contestHistory: z.object({ + totalNum: z.number(), + contests: z.array( + ContestSchema.extend({ + description: z.string().nullable().optional(), + }) + ), + }), +}); + +export const ContestDetailResponseSchema = z.object({ + contest: ContestDetailSchema.nullable().optional(), }); // --- Submission Schemas --- @@ -304,6 +347,12 @@ export const UserStatusSchema = z.object({ export type ValidatedProblem = z.infer; export type ValidatedProblemDetail = z.infer; export type ValidatedDailyChallenge = z.infer; +export type ValidatedContest = z.infer; +export type ValidatedContestQuestion = z.infer; +export type ValidatedContestDetail = z.infer; +export type ValidatedContestList = z.infer; +export type ValidatedCnContestHistory = z.infer; +export type ValidatedContestDetailResponse = z.infer; export type ValidatedSubmission = z.infer; export type ValidatedSubmissionDetails = z.infer; export type ValidatedTestResult = z.infer; diff --git a/src/types.ts b/src/types.ts index be2cfc9..aa7be76 100644 --- a/src/types.ts +++ b/src/types.ts @@ -74,6 +74,29 @@ export interface DailyChallenge { question: Problem; } +export interface Contest { + title: string; + titleSlug: string; + startTime: number; + duration: number; + originStartTime?: number | null; + isVirtual?: boolean | null; + containsPremium?: boolean | null; +} + +export interface ContestQuestion { + questionId: string; + questionFrontendId?: string; + title: string; + titleSlug: string; + difficulty?: 'Easy' | 'Medium' | 'Hard' | null; +} + +export interface ContestDetail extends Contest { + description?: string | null; + questions: ContestQuestion[]; +} + export interface SubmissionResult { status_code: number; status_msg: string; From 7cc3a0c052f57baf89bd4ce9a548b48219a6023d Mon Sep 17 00:00:00 2001 From: Anurag Deo Date: Sun, 26 Jul 2026 13:29:17 +0530 Subject: [PATCH 04/14] feat(api): add site-aware contest queries Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/__tests__/api/query-resolver.test.ts | 28 ++++++++++ src/api/client.ts | 70 +++++++++++++++++++++++- src/api/queries.cn.ts | 23 ++++++++ src/api/queries.global.ts | 38 +++++++++++++ 4 files changed, 158 insertions(+), 1 deletion(-) diff --git a/src/__tests__/api/query-resolver.test.ts b/src/__tests__/api/query-resolver.test.ts index 72a678d..6ed3c15 100644 --- a/src/__tests__/api/query-resolver.test.ts +++ b/src/__tests__/api/query-resolver.test.ts @@ -3,7 +3,13 @@ import { getQueryPack } from '../../api/query-resolver.js'; import { PROBLEM_LIST_QUERY as PROBLEM_LIST_QUERY_GLOBAL } from '../../api/queries.global.js'; import { PROBLEM_DETAIL_QUERY as PROBLEM_DETAIL_QUERY_GLOBAL } from '../../api/queries.global.js'; import { DAILY_CHALLENGE_QUERY as DAILY_CHALLENGE_QUERY_GLOBAL } from '../../api/queries.global.js'; +import { CONTEST_DETAIL_QUERY as CONTEST_DETAIL_QUERY_GLOBAL } from '../../api/queries.global.js'; +import { CONTEST_LIST_QUERY as CONTEST_LIST_QUERY_GLOBAL } from '../../api/queries.global.js'; import { DAILY_CHALLENGE_QUERY_CN } from '../../api/queries.cn.js'; +import { + CONTEST_DETAIL_QUERY_CN, + CONTEST_LIST_QUERY_CN, +} from '../../api/queries.cn.js'; describe('query resolver', () => { it('returns global query pack for leetcode.com', () => { @@ -23,4 +29,26 @@ describe('query resolver', () => { expect(pack.PROBLEM_DETAIL_QUERY).toContain('translatedTitle'); expect(pack.PROBLEM_DETAIL_QUERY).toContain('translatedContent'); }); + + it('resolves contest queries for both supported sites', () => { + const globalPack = getQueryPack('leetcode.com'); + const cnPack = getQueryPack('leetcode.cn'); + + expect(globalPack.CONTEST_LIST_QUERY).toBe(CONTEST_LIST_QUERY_GLOBAL); + expect(globalPack.CONTEST_LIST_QUERY).toContain('allContests'); + expect(globalPack.CONTEST_DETAIL_QUERY).toBe(CONTEST_DETAIL_QUERY_GLOBAL); + expect(globalPack.CONTEST_DETAIL_QUERY).toContain('contest(titleSlug:'); + expect(globalPack.CONTEST_DETAIL_QUERY).toMatch( + /questions\s*\{\s*questionId\s+title\s+titleSlug\s*\}/ + ); + expect(globalPack.CONTEST_DETAIL_QUERY).not.toContain('difficulty'); + expect(cnPack.CONTEST_LIST_QUERY).toBe(CONTEST_LIST_QUERY_CN); + expect(cnPack.CONTEST_LIST_QUERY).toContain('contestHistory(pageNum:'); + expect(cnPack.CONTEST_LIST_QUERY).toContain('totalNum'); + expect(cnPack.CONTEST_DETAIL_QUERY).toBe(CONTEST_DETAIL_QUERY_CN); + expect(cnPack.CONTEST_DETAIL_QUERY).toMatch( + /questions\s*\{\s*questionId\s+title\s+titleSlug\s*\}/ + ); + expect(cnPack.CONTEST_DETAIL_QUERY).not.toContain('difficulty'); + }); }); diff --git a/src/api/client.ts b/src/api/client.ts index b655130..90c8481 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -2,6 +2,8 @@ import got, { Got } from 'got'; import { z } from 'zod'; import type { + Contest, + ContestDetail, DailyChallenge, LeetCodeCredentials, LeetCodeSite, @@ -17,8 +19,11 @@ import { CnProblemDetailSchema, CnProblemListSchema, CnDailyChallengeSchema, + CnContestHistorySchema, CnSkillStatsSchema, CnUserProfileSchema, + ContestDetailResponseSchema, + ContestListSchema, DailyChallengeSchema, ProblemDetailSchema, ProblemSchema, @@ -53,7 +58,9 @@ type GraphQLOperation = | 'USER_PROFILE' | 'SKILL_STATS' | 'SUBMISSION_LIST' - | 'SUBMISSION_DETAILS'; + | 'SUBMISSION_DETAILS' + | 'CONTEST_LIST' + | 'CONTEST_DETAIL'; const OPERATION_LABEL: Record = { USER_STATUS: 'user status', @@ -65,6 +72,8 @@ const OPERATION_LABEL: Record = { SKILL_STATS: 'skill stats', SUBMISSION_LIST: 'submission list', SUBMISSION_DETAILS: 'submission details', + CONTEST_LIST: 'contest list', + CONTEST_DETAIL: 'contest detail', }; function isSchemaMismatchError(message: string): boolean { @@ -324,6 +333,65 @@ export class LeetCodeClient { return validated as DailyChallenge; } + async getContests(): Promise { + if (this.site === 'leetcode.cn') { + const pageSize = 100; + const contests: Contest[] = []; + let pageNum = 1; + let totalNum = 0; + + do { + const data = await this.graphql( + 'CONTEST_LIST', + this.queries.CONTEST_LIST_QUERY, + { pageNum, pageSize } + ); + const validated = CnContestHistorySchema.parse(data); + const page = validated.contestHistory.contests; + + if (pageNum === 1) { + totalNum = validated.contestHistory.totalNum; + } + + if (totalNum > contests.length && page.length === 0) { + throw new Error('LeetCode CN contest history returned an incomplete page'); + } + + contests.push( + ...page.map((contest) => ({ + title: contest.title, + titleSlug: contest.titleSlug, + startTime: contest.startTime, + duration: contest.duration, + originStartTime: contest.originStartTime, + isVirtual: contest.isVirtual, + containsPremium: contest.containsPremium, + })) + ); + pageNum += 1; + } while (contests.length < totalNum); + + return contests; + } + + const data = await this.graphql('CONTEST_LIST', this.queries.CONTEST_LIST_QUERY); + const validated = ContestListSchema.parse(data); + return validated.allContests; + } + + async getContest(titleSlug: string): Promise { + const data = await this.graphql('CONTEST_DETAIL', this.queries.CONTEST_DETAIL_QUERY, { + titleSlug, + }); + const validated = ContestDetailResponseSchema.parse(data); + + if (!validated.contest) { + throw new Error(`Contest "${titleSlug}" not found`); + } + + return validated.contest; + } + async getRandomProblem(filters: ProblemListFilters = {}): Promise { const variables: Record = { categorySlug: '', diff --git a/src/api/queries.cn.ts b/src/api/queries.cn.ts index 450628a..a3a6507 100644 --- a/src/api/queries.cn.ts +++ b/src/api/queries.cn.ts @@ -1,6 +1,7 @@ // GraphQL queries for leetcode.cn (China schema) import { DAILY_CHALLENGE_QUERY as DAILY_CHALLENGE_QUERY_GLOBAL, + CONTEST_DETAIL_QUERY as CONTEST_DETAIL_QUERY_GLOBAL, RANDOM_PROBLEM_QUERY, SUBMISSION_DETAILS_QUERY, SUBMISSION_LIST_QUERY, @@ -156,6 +157,26 @@ export const SKILL_STATS_QUERY_CN = ` } `; +export const CONTEST_LIST_QUERY_CN = ` + query contestHistory($pageNum: Int!, $pageSize: Int) { + contestHistory(pageNum: $pageNum, pageSize: $pageSize) { + totalNum + contests { + containsPremium + title + titleSlug + description + startTime + duration + originStartTime + isVirtual + } + } + } +`; + +export const CONTEST_DETAIL_QUERY_CN = CONTEST_DETAIL_QUERY_GLOBAL; + export const CN_QUERY_PACK: QueryPack = { PROBLEM_LIST_QUERY: PROBLEM_LIST_QUERY_CN, PROBLEM_DETAIL_QUERY: PROBLEM_DETAIL_QUERY_CN, @@ -166,6 +187,8 @@ export const CN_QUERY_PACK: QueryPack = { SUBMISSION_LIST_QUERY, RANDOM_PROBLEM_QUERY, SUBMISSION_DETAILS_QUERY, + CONTEST_LIST_QUERY: CONTEST_LIST_QUERY_CN, + CONTEST_DETAIL_QUERY: CONTEST_DETAIL_QUERY_CN, }; // Export for testing or targeted use diff --git a/src/api/queries.global.ts b/src/api/queries.global.ts index 4002e11..5e23ce0 100644 --- a/src/api/queries.global.ts +++ b/src/api/queries.global.ts @@ -183,6 +183,40 @@ export const SUBMISSION_DETAILS_QUERY = ` } `; +export const CONTEST_LIST_QUERY = ` + query allContests { + allContests { + title + titleSlug + startTime + duration + originStartTime + isVirtual + containsPremium + } + } +`; + +export const CONTEST_DETAIL_QUERY = ` + query contest($titleSlug: String!) { + contest(titleSlug: $titleSlug) { + title + titleSlug + startTime + duration + originStartTime + isVirtual + containsPremium + description + questions { + questionId + title + titleSlug + } + } + } +`; + export interface QueryPack { PROBLEM_LIST_QUERY: string; PROBLEM_DETAIL_QUERY: string; @@ -193,6 +227,8 @@ export interface QueryPack { SUBMISSION_LIST_QUERY: string; RANDOM_PROBLEM_QUERY: string; SUBMISSION_DETAILS_QUERY: string; + CONTEST_LIST_QUERY: string; + CONTEST_DETAIL_QUERY: string; } export const GLOBAL_QUERY_PACK: QueryPack = { @@ -205,4 +241,6 @@ export const GLOBAL_QUERY_PACK: QueryPack = { SUBMISSION_LIST_QUERY, RANDOM_PROBLEM_QUERY, SUBMISSION_DETAILS_QUERY, + CONTEST_LIST_QUERY, + CONTEST_DETAIL_QUERY, }; From 958249a7e43123716ebc4c53d5a9391e7c85e83a Mon Sep 17 00:00:00 2001 From: Anurag Deo Date: Sun, 26 Jul 2026 13:29:26 +0530 Subject: [PATCH 05/14] feat: add contest problem selection Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/__tests__/commands/contest.test.ts | 145 +++++++++++++++++++++++ src/commands/contest.ts | 152 +++++++++++++++++++++++++ 2 files changed, 297 insertions(+) create mode 100644 src/__tests__/commands/contest.test.ts create mode 100644 src/commands/contest.ts diff --git a/src/__tests__/commands/contest.test.ts b/src/__tests__/commands/contest.test.ts new file mode 100644 index 0000000..6f5110a --- /dev/null +++ b/src/__tests__/commands/contest.test.ts @@ -0,0 +1,145 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +const getContests = vi.hoisted(() => vi.fn()); +const getContest = vi.hoisted(() => vi.fn()); +const prompt = vi.hoisted(() => vi.fn()); +const pickCommand = vi.hoisted(() => vi.fn().mockResolvedValue(true)); +const spinner = vi.hoisted(() => ({ + start: vi.fn().mockReturnThis(), + stop: vi.fn().mockReturnThis(), + succeed: vi.fn().mockReturnThis(), + fail: vi.fn().mockReturnThis(), +})); + +vi.mock('../../api/client.js', () => ({ + leetcodeClient: { getContests, getContest }, +})); + +vi.mock('../../utils/auth.js', () => ({ + requireAuth: vi.fn().mockResolvedValue({ authorized: true }), +})); + +vi.mock('../../commands/pick.js', () => ({ pickCommand })); + +vi.mock('inquirer', () => ({ + default: { prompt }, +})); + +vi.mock('ora', () => ({ + default: vi.fn(() => spinner), +})); + +import { contestCommand } from '../../commands/contest.js'; +import { outputContains } from '../setup.js'; + +describe('Contest Command', () => { + beforeEach(() => { + vi.clearAllMocks(); + getContests.mockResolvedValue([ + { title: 'Weekly Contest 1', titleSlug: 'weekly-contest-1' }, + { title: 'Biweekly Contest 1', titleSlug: 'biweekly-contest-1' }, + ]); + getContest.mockResolvedValue({ + title: 'Biweekly Contest 1', + titleSlug: 'biweekly-contest-1', + startTime: 0, + duration: 0, + questions: [ + { + questionId: '1', + title: 'Contest Problem 1', + titleSlug: 'contest-problem-1', + }, + { + questionId: '2', + title: 'Contest Problem 2', + titleSlug: 'contest-problem-2', + }, + ], + }); + pickCommand.mockResolvedValue(true); + }); + + it('selects contests and problems in API order', async () => { + prompt + .mockResolvedValueOnce({ contestSlug: 'biweekly-contest-1' }) + .mockResolvedValueOnce({ questionSlug: 'contest-problem-2' }); + + await contestCommand(undefined, { lang: 'python3', open: false }); + + expect(prompt).toHaveBeenCalledTimes(2); + expect(prompt.mock.calls[0][0][0].choices).toEqual([ + { name: 'Weekly Contest 1 (weekly-contest-1)', value: 'weekly-contest-1' }, + { name: 'Biweekly Contest 1 (biweekly-contest-1)', value: 'biweekly-contest-1' }, + ]); + expect(prompt.mock.calls[1][0][0].choices).toEqual([ + { name: '1. Contest Problem 1 (contest-problem-1)', value: 'contest-problem-1' }, + { name: '2. Contest Problem 2 (contest-problem-2)', value: 'contest-problem-2' }, + ]); + expect(getContest).toHaveBeenCalledWith('biweekly-contest-1'); + expect(pickCommand).toHaveBeenCalledWith('contest-problem-2', { + lang: 'python3', + open: false, + }); + }); + + it('supports a direct contest slug without fetching the contest list', async () => { + prompt.mockResolvedValueOnce({ questionSlug: 'contest-problem-1' }); + + await contestCommand('weekly-contest-1'); + + expect(getContests).not.toHaveBeenCalled(); + expect(getContest).toHaveBeenCalledWith('weekly-contest-1'); + expect(pickCommand).toHaveBeenCalledWith('contest-problem-1', {}); + expect(spinner.stop).toHaveBeenCalledTimes(2); + }); + + it('handles empty contest lists and unavailable contests', async () => { + getContests.mockResolvedValueOnce([]); + await contestCommand(); + expect(prompt).not.toHaveBeenCalled(); + + getContest.mockResolvedValueOnce(null); + await contestCommand('missing-contest'); + expect(prompt).not.toHaveBeenCalled(); + }); + + it('rejects malicious contest problem slugs', async () => { + getContest.mockResolvedValueOnce({ + title: 'Weekly Contest 1', + titleSlug: 'weekly-contest-1', + startTime: 0, + duration: 0, + questions: [ + { + questionId: 'outside', + title: 'Outside', + titleSlug: '../outside', + }, + ], + }); + prompt.mockResolvedValueOnce({ questionSlug: '../outside' }); + + await contestCommand('weekly-contest-1'); + + expect(pickCommand).not.toHaveBeenCalled(); + expect(outputContains('unavailable or has an invalid slug')).toBe(true); + }); + + it('handles prompt cancellation without selecting a problem', async () => { + const cancellation = new Error('User force closed the prompt'); + cancellation.name = 'ExitPromptError'; + prompt.mockRejectedValueOnce(cancellation); + + await expect(contestCommand()).resolves.toBeUndefined(); + expect(getContest).not.toHaveBeenCalled(); + expect(pickCommand).not.toHaveBeenCalled(); + }); + + it('handles contest API errors without throwing', async () => { + getContests.mockRejectedValueOnce(new Error('API unavailable')); + + await expect(contestCommand()).resolves.toBeUndefined(); + expect(pickCommand).not.toHaveBeenCalled(); + }); +}); diff --git a/src/commands/contest.ts b/src/commands/contest.ts new file mode 100644 index 0000000..c938555 --- /dev/null +++ b/src/commands/contest.ts @@ -0,0 +1,152 @@ +// Contest command - browse contests and generate a solution for a contest problem +import inquirer from 'inquirer'; +import ora from 'ora'; +import chalk from 'chalk'; +import { leetcodeClient } from '../api/client.js'; +import type { Contest, ContestDetail } from '../types.js'; +import { requireAuth } from '../utils/auth.js'; +import { pickCommand } from './pick.js'; + +export interface ContestOptions { + lang?: string; + open?: boolean; +} + +const LEETCODE_SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/; + +function isPromptCancellation(error: unknown): boolean { + return ( + error instanceof Error && + (error.name === 'ExitPromptError' || error.message.toLowerCase().includes('force closed')) + ); +} + +function getContestLabel(contest: Contest): string { + return contest.titleSlug === contest.title + ? contest.title + : `${contest.title} (${contest.titleSlug})`; +} + +function getQuestionChoices(contest: ContestDetail): Array<{ name: string; value: string }> { + return contest.questions.map((question, index) => ({ + name: `${index + 1}. ${question.title} (${question.titleSlug})`, + value: question.titleSlug, + })); +} + +export async function contestCommand( + contestSlug?: string, + options: ContestOptions = {} +): Promise { + const { authorized } = await requireAuth(); + if (!authorized) return; + + let spinner = ora({ text: 'Fetching contests...', spinner: 'dots' }).start(); + let spinnerRunning = true; + let operation = 'fetch contests'; + let selectedContestSlug = contestSlug; + + const stopSpinner = (): void => { + if (spinnerRunning) { + spinner.stop(); + spinnerRunning = false; + } + }; + + const failSpinner = (message: string): void => { + if (spinnerRunning) { + spinner.fail(message); + spinnerRunning = false; + } + }; + + try { + if (!selectedContestSlug) { + const contests = await leetcodeClient.getContests(); + stopSpinner(); + + if (contests.length === 0) { + console.log(chalk.yellow('No contests are available.')); + return; + } + + operation = 'select contest'; + const answer = await inquirer.prompt([ + { + type: 'list', + name: 'contestSlug', + message: 'Select a contest:', + choices: contests.map((contest) => ({ + name: getContestLabel(contest), + value: contest.titleSlug, + })), + }, + ]); + + selectedContestSlug = answer.contestSlug; + if (typeof selectedContestSlug !== 'string' || selectedContestSlug.length === 0) { + console.log(chalk.yellow('Contest selection cancelled.')); + return; + } + } + + stopSpinner(); + operation = 'fetch contest'; + spinner = ora({ text: 'Fetching contest problems...', spinner: 'dots' }).start(); + spinnerRunning = true; + const contest = await leetcodeClient.getContest(selectedContestSlug); + stopSpinner(); + + if (!contest) { + console.log(chalk.yellow(`Contest "${selectedContestSlug}" is unavailable.`)); + return; + } + + const choices = getQuestionChoices(contest); + if (choices.length === 0) { + console.log(chalk.yellow(`Contest "${getContestLabel(contest)}" has no problems available.`)); + return; + } + + operation = 'select contest problem'; + const answer = await inquirer.prompt([ + { + type: 'list', + name: 'questionSlug', + message: `Select a problem from ${getContestLabel(contest)}:`, + choices, + }, + ]); + + if (typeof answer.questionSlug !== 'string' || answer.questionSlug.length === 0) { + console.log(chalk.yellow('Contest problem selection cancelled.')); + return; + } + + if (!LEETCODE_SLUG_PATTERN.test(answer.questionSlug)) { + console.log( + chalk.yellow( + `Contest problem "${answer.questionSlug}" is unavailable or has an invalid slug.` + ) + ); + return; + } + + await pickCommand(answer.questionSlug, options); + } catch (error) { + if (isPromptCancellation(error)) { + stopSpinner(); + console.log(chalk.yellow('Contest selection cancelled.')); + return; + } + + if (operation === 'fetch contest' && error instanceof Error && /not found/i.test(error.message)) { + failSpinner('Contest unavailable'); + console.log(chalk.yellow(`Contest "${selectedContestSlug ?? ''}" is unavailable.`)); + return; + } + + failSpinner(`Failed to ${operation}`); + console.log(chalk.red(error instanceof Error ? error.message : 'Unknown error')); + } +} From a7616320c54d19533ddc74b5b6614b30ee938bd5 Mon Sep 17 00:00:00 2001 From: Anurag Deo Date: Sun, 26 Jul 2026 13:29:35 +0530 Subject: [PATCH 06/14] feat: register contest command Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/commands/completion.ts | 73 ++++++++++++++++++++++++-------------- src/index.ts | 19 ++++++++++ 2 files changed, 65 insertions(+), 27 deletions(-) diff --git a/src/commands/completion.ts b/src/commands/completion.ts index 230532e..8e1f852 100644 --- a/src/commands/completion.ts +++ b/src/commands/completion.ts @@ -6,7 +6,7 @@ const BASH_COMPLETION = `_leetcode_completion() { cur="\${COMP_WORDS[COMP_CWORD]}" prev="\${COMP_WORDS[COMP_CWORD-1]}" - local commands="login logout whoami list show pick pick-batch random test submit submissions diff timer workspace bookmark note sync today stat changelog update star" + local commands="login logout whoami list show pick pick-batch contest random test submit submissions diff timer workspace bookmark note sync today stat changelog update star" # Find command in the command line local cmd="" @@ -69,6 +69,14 @@ const BASH_COMPLETION = `_leetcode_completion() { COMPREPLY=( \$(compgen -W "\$options" -- "\$cur") ) fi ;; + contest) + local options="-l --lang --no-open" + if [[ "\$prev" == "-l" || "\$prev" == "--lang" ]]; then + COMPREPLY=( \$(compgen -W "typescript javascript python3 java cpp c csharp go rust kotlin swift sql" -- "\$cur") ) + else + COMPREPLY=( \$(compgen -W "\$options" -- "\$cur") ) + fi + ;; pick-batch|random) local options="-d --difficulty -t --tag -l --limit -o --offset -p --pick --no-open" if [[ "\$prev" == "-d" || "\$prev" == "--difficulty" ]]; then @@ -126,9 +134,10 @@ _leetcode() { 'whoami:Show current user profile' 'list:List problems' 'show:Show problem details' - 'pick:Pick and generate solution file' - 'pick-batch:Pick multiple problems' - 'random:Pick a random problem' + 'pick:Pick and generate solution file' + 'pick-batch:Pick multiple problems' + 'contest:Browse contests and pick a problem' + 'random:Pick a random problem' 'test:Test your solution' 'submit:Submit your solution' 'submissions:View past submissions' @@ -217,7 +226,12 @@ _leetcode() { _arguments \\ '(-l --language)'{-l,--language}'[Specify programming language]:language:(typescript javascript python3 java cpp c csharp go rust kotlin swift sql)' \\ '(-e --editor)'{-e,--editor}'[Open solution file in editor]' \\ - '--no-open[Do not open problem details or file]' + '--no-open[Do not open problem details or file]' + ;; + contest) + _arguments \\ + '(-l --lang)'{-l,--lang}'[Specify programming language]:language:(typescript javascript python3 java cpp c csharp go rust kotlin swift sql)' \\ + '--no-open[Do not open solution file in editor]' ;; pick-batch) _arguments \\ @@ -280,28 +294,29 @@ const FISH_COMPLETION = `# Disable standard file completion for commands complete -c leetcode -f # Main commands -complete -c leetcode -n "not __fish_seen_subcommand_from login logout whoami list show pick pick-batch random test submit submissions diff timer workspace bookmark note sync today stat changelog update star" -a login -d "Login to LeetCode" -complete -c leetcode -n "not __fish_seen_subcommand_from login logout whoami list show pick pick-batch random test submit submissions diff timer workspace bookmark note sync today stat changelog update star" -a logout -d "Logout from LeetCode" -complete -c leetcode -n "not __fish_seen_subcommand_from login logout whoami list show pick pick-batch random test submit submissions diff timer workspace bookmark note sync today stat changelog update star" -a whoami -d "Show current user profile" -complete -c leetcode -n "not __fish_seen_subcommand_from login logout whoami list show pick pick-batch random test submit submissions diff timer workspace bookmark note sync today stat changelog update star" -a list -d "List problems" -complete -c leetcode -n "not __fish_seen_subcommand_from login logout whoami list show pick pick-batch random test submit submissions diff timer workspace bookmark note sync today stat changelog update star" -a show -d "Show problem details" -complete -c leetcode -n "not __fish_seen_subcommand_from login logout whoami list show pick pick-batch random test submit submissions diff timer workspace bookmark note sync today stat changelog update star" -a pick -d "Pick and generate solution file" -complete -c leetcode -n "not __fish_seen_subcommand_from login logout whoami list show pick pick-batch random test submit submissions diff timer workspace bookmark note sync today stat changelog update star" -a pick-batch -d "Pick multiple problems" -complete -c leetcode -n "not __fish_seen_subcommand_from login logout whoami list show pick pick-batch random test submit submissions diff timer workspace bookmark note sync today stat changelog update star" -a random -d "Pick a random problem" -complete -c leetcode -n "not __fish_seen_subcommand_from login logout whoami list show pick pick-batch random test submit submissions diff timer workspace bookmark note sync today stat changelog update star" -a test -d "Test your solution" -complete -c leetcode -n "not __fish_seen_subcommand_from login logout whoami list show pick pick-batch random test submit submissions diff timer workspace bookmark note sync today stat changelog update star" -a submit -d "Submit your solution" -complete -c leetcode -n "not __fish_seen_subcommand_from login logout whoami list show pick pick-batch random test submit submissions diff timer workspace bookmark note sync today stat changelog update star" -a submissions -d "View past submissions" -complete -c leetcode -n "not __fish_seen_subcommand_from login logout whoami list show pick pick-batch random test submit submissions diff timer workspace bookmark note sync today stat changelog update star" -a diff -d "Compare solution with past submission" -complete -c leetcode -n "not __fish_seen_subcommand_from login logout whoami list show pick pick-batch random test submit submissions diff timer workspace bookmark note sync today stat changelog update star" -a timer -d "Manage interview timer" -complete -c leetcode -n "not __fish_seen_subcommand_from login logout whoami list show pick pick-batch random test submit submissions diff timer workspace bookmark note sync today stat changelog update star" -a workspace -d "Manage workspaces" -complete -c leetcode -n "not __fish_seen_subcommand_from login logout whoami list show pick pick-batch random test submit submissions diff timer workspace bookmark note sync today stat changelog update star" -a bookmark -d "Manage bookmarked problems" -complete -c leetcode -n "not __fish_seen_subcommand_from login logout whoami list show pick pick-batch random test submit submissions diff timer workspace bookmark note sync today stat changelog update star" -a note -d "Manage personal notes" -complete -c leetcode -n "not __fish_seen_subcommand_from login logout whoami list show pick pick-batch random test submit submissions diff timer workspace bookmark note sync today stat changelog update star" -a sync -d "Sync solutions to Git repository" -complete -c leetcode -n "not __fish_seen_subcommand_from login logout whoami list show pick pick-batch random test submit submissions diff timer workspace bookmark note sync today stat changelog update star" -a today -d "Show today progress" -complete -c leetcode -n "not __fish_seen_subcommand_from login logout whoami list show pick pick-batch random test submit submissions diff timer workspace bookmark note sync today stat changelog update star" -a stat -d "Show solving statistics" -complete -c leetcode -n "not __fish_seen_subcommand_from login logout whoami list show pick pick-batch random test submit submissions diff timer workspace bookmark note sync today stat changelog update star" -a changelog -d "Show version changelog" -complete -c leetcode -n "not __fish_seen_subcommand_from login logout whoami list show pick pick-batch random test submit submissions diff timer workspace bookmark note sync today stat changelog update star" -a update -d "Check for updates" -complete -c leetcode -n "not __fish_seen_subcommand_from login logout whoami list show pick pick-batch random test submit submissions diff timer workspace bookmark note sync today stat changelog update star" -a star -d "Open GitHub repo to star the project" +complete -c leetcode -n "not __fish_seen_subcommand_from login logout whoami list show pick pick-batch contest random test submit submissions diff timer workspace bookmark note sync today stat changelog update star" -a login -d "Login to LeetCode" +complete -c leetcode -n "not __fish_seen_subcommand_from login logout whoami list show pick pick-batch contest random test submit submissions diff timer workspace bookmark note sync today stat changelog update star" -a logout -d "Logout from LeetCode" +complete -c leetcode -n "not __fish_seen_subcommand_from login logout whoami list show pick pick-batch contest random test submit submissions diff timer workspace bookmark note sync today stat changelog update star" -a whoami -d "Show current user profile" +complete -c leetcode -n "not __fish_seen_subcommand_from login logout whoami list show pick pick-batch contest random test submit submissions diff timer workspace bookmark note sync today stat changelog update star" -a list -d "List problems" +complete -c leetcode -n "not __fish_seen_subcommand_from login logout whoami list show pick pick-batch contest random test submit submissions diff timer workspace bookmark note sync today stat changelog update star" -a show -d "Show problem details" +complete -c leetcode -n "not __fish_seen_subcommand_from login logout whoami list show pick pick-batch contest random test submit submissions diff timer workspace bookmark note sync today stat changelog update star" -a pick -d "Pick and generate solution file" +complete -c leetcode -n "not __fish_seen_subcommand_from login logout whoami list show pick pick-batch contest random test submit submissions diff timer workspace bookmark note sync today stat changelog update star" -a pick-batch -d "Pick multiple problems" +complete -c leetcode -n "not __fish_seen_subcommand_from login logout whoami list show pick pick-batch contest random test submit submissions diff timer workspace bookmark note sync today stat changelog update star" -a contest -d "Browse contests and pick a problem" +complete -c leetcode -n "not __fish_seen_subcommand_from login logout whoami list show pick pick-batch contest random test submit submissions diff timer workspace bookmark note sync today stat changelog update star" -a random -d "Pick a random problem" +complete -c leetcode -n "not __fish_seen_subcommand_from login logout whoami list show pick pick-batch contest random test submit submissions diff timer workspace bookmark note sync today stat changelog update star" -a test -d "Test your solution" +complete -c leetcode -n "not __fish_seen_subcommand_from login logout whoami list show pick pick-batch contest random test submit submissions diff timer workspace bookmark note sync today stat changelog update star" -a submit -d "Submit your solution" +complete -c leetcode -n "not __fish_seen_subcommand_from login logout whoami list show pick pick-batch contest random test submit submissions diff timer workspace bookmark note sync today stat changelog update star" -a submissions -d "View past submissions" +complete -c leetcode -n "not __fish_seen_subcommand_from login logout whoami list show pick pick-batch contest random test submit submissions diff timer workspace bookmark note sync today stat changelog update star" -a diff -d "Compare solution with past submission" +complete -c leetcode -n "not __fish_seen_subcommand_from login logout whoami list show pick pick-batch contest random test submit submissions diff timer workspace bookmark note sync today stat changelog update star" -a timer -d "Manage interview timer" +complete -c leetcode -n "not __fish_seen_subcommand_from login logout whoami list show pick pick-batch contest random test submit submissions diff timer workspace bookmark note sync today stat changelog update star" -a workspace -d "Manage workspaces" +complete -c leetcode -n "not __fish_seen_subcommand_from login logout whoami list show pick pick-batch contest random test submit submissions diff timer workspace bookmark note sync today stat changelog update star" -a bookmark -d "Manage bookmarked problems" +complete -c leetcode -n "not __fish_seen_subcommand_from login logout whoami list show pick pick-batch contest random test submit submissions diff timer workspace bookmark note sync today stat changelog update star" -a note -d "Manage personal notes" +complete -c leetcode -n "not __fish_seen_subcommand_from login logout whoami list show pick pick-batch contest random test submit submissions diff timer workspace bookmark note sync today stat changelog update star" -a sync -d "Sync solutions to Git repository" +complete -c leetcode -n "not __fish_seen_subcommand_from login logout whoami list show pick pick-batch contest random test submit submissions diff timer workspace bookmark note sync today stat changelog update star" -a today -d "Show today progress" +complete -c leetcode -n "not __fish_seen_subcommand_from login logout whoami list show pick pick-batch contest random test submit submissions diff timer workspace bookmark note sync today stat changelog update star" -a stat -d "Show solving statistics" +complete -c leetcode -n "not __fish_seen_subcommand_from login logout whoami list show pick pick-batch contest random test submit submissions diff timer workspace bookmark note sync today stat changelog update star" -a changelog -d "Show version changelog" +complete -c leetcode -n "not __fish_seen_subcommand_from login logout whoami list show pick pick-batch contest random test submit submissions diff timer workspace bookmark note sync today stat changelog update star" -a update -d "Check for updates" +complete -c leetcode -n "not __fish_seen_subcommand_from login logout whoami list show pick pick-batch contest random test submit submissions diff timer workspace bookmark note sync today stat changelog update star" -a star -d "Open GitHub repo to star the project" # Workspace subcommands complete -c leetcode -n "__fish_seen_subcommand_from workspace" -a current -d "Show active workspace" @@ -336,6 +351,10 @@ complete -c leetcode -n "__fish_seen_subcommand_from list" -s t -l tag -d "Filte complete -c leetcode -n "__fish_seen_subcommand_from list" -s q -l search -d "Search by keyword" complete -c leetcode -n "__fish_seen_subcommand_from list" -s l -l limit -d "Limit results" complete -c leetcode -n "__fish_seen_subcommand_from list" -s o -l offset -d "Offset results" + +# Contest options +complete -c leetcode -n "__fish_seen_subcommand_from contest" -s l -l lang -x -a "typescript javascript python3 java cpp c csharp go rust kotlin swift sql" -d "Specify programming language" +complete -c leetcode -n "__fish_seen_subcommand_from contest" -l no-open -d "Do not open solution file in editor" `; export async function completionCommand(shell: string): Promise { diff --git a/src/index.ts b/src/index.ts index 038111a..6615c61 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,6 +7,7 @@ import { listCommand } from './commands/list.js'; import { showCommand } from './commands/show.js'; import { hintCommand } from './commands/hint.js'; import { pickCommand, batchPickCommand } from './commands/pick.js'; +import { contestCommand } from './commands/contest.js'; import { testCommand } from './commands/test.js'; import { submitCommand } from './commands/submit.js'; import { statCommand } from './commands/stat.js'; @@ -239,6 +240,24 @@ ${chalk.yellow('Examples:')} ) .action(batchPickCommand); +program + .command('contest [slug]') + .alias('c') + .description('Browse contests and generate a solution for a contest problem') + .option('-l, --lang ', 'Programming language for the solution') + .option('--no-open', 'Do not open file in editor') + .addHelpText( + 'after', + ` +${chalk.yellow('Examples:')} + ${chalk.cyan('$ leetcode contest')} Browse available contests + ${chalk.cyan('$ leetcode contest weekly-contest-1')} Open a contest by slug + ${chalk.cyan('$ leetcode contest -l python3')} Pick with specific language + ${chalk.cyan('$ leetcode contest --no-open')} Create file without opening +` + ) + .action((slug, options) => contestCommand(slug, options)); + program .command('test ') .alias('t') From 696dd79088acaac624b67163b4900ce42e5f3472 Mon Sep 17 00:00:00 2001 From: Anurag Deo Date: Sun, 26 Jul 2026 13:29:45 +0530 Subject: [PATCH 07/14] feat: support zed editor Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/__tests__/utils/editor.test.ts | 96 ++++++++++++++++++++++++++++++ src/utils/editor.ts | 26 ++++++-- 2 files changed, 117 insertions(+), 5 deletions(-) create mode 100644 src/__tests__/utils/editor.test.ts diff --git a/src/__tests__/utils/editor.test.ts b/src/__tests__/utils/editor.test.ts new file mode 100644 index 0000000..8850856 --- /dev/null +++ b/src/__tests__/utils/editor.test.ts @@ -0,0 +1,96 @@ +import { EventEmitter } from 'node:events'; +import { describe, expect, it, vi } from 'vitest'; + +const { editorMock, workDirMock, spawnMock, openMock } = vi.hoisted(() => ({ + editorMock: vi.fn(() => 'zed'), + workDirMock: vi.fn(() => '/tmp/leetcode'), + spawnMock: vi.fn(), + openMock: vi.fn(), +})); + +vi.mock('../../storage/config.js', () => ({ + config: { + getEditor: editorMock, + getWorkDir: workDirMock, + }, +})); + +vi.mock('child_process', () => ({ spawn: spawnMock })); +vi.mock('open', () => ({ default: openMock })); + +import { openInEditor } from '../../utils/editor.js'; + +function createChildProcess() { + return Object.assign(new EventEmitter(), { unref: vi.fn() }); +} + +describe('openInEditor', () => { + it.each(['zed', 'zeditor', 'zed.exe'])('launches %s with only the file path', async (editor) => { + const child = createChildProcess(); + editorMock.mockReturnValueOnce(editor); + spawnMock.mockReturnValueOnce(child); + + const opening = openInEditor('/tmp/leetcode/Easy/Array/1.two-sum.ts', '/tmp/leetcode'); + child.emit('spawn'); + await opening; + + expect(spawnMock).toHaveBeenCalledWith( + editor, + ['/tmp/leetcode/Easy/Array/1.two-sum.ts'], + { detached: true, stdio: 'ignore' } + ); + expect(child.unref).toHaveBeenCalledOnce(); + expect(openMock).not.toHaveBeenCalled(); + }); + + it('reports a missing Zed executable with the friendly editor message', async () => { + const child = createChildProcess(); + editorMock.mockReturnValueOnce('zed'); + spawnMock.mockReturnValueOnce(child); + + const opening = openInEditor('/tmp/solution.ts'); + child.emit('error', new Error('spawn zed ENOENT')); + + await expect(opening).rejects.toThrow( + "Failed to open editor 'zed'. Make sure it is installed and in your PATH." + ); + expect(child.unref).toHaveBeenCalledOnce(); + }); + + it('preserves the existing VS Code launch arguments', async () => { + const child = createChildProcess(); + editorMock.mockReturnValueOnce('code'); + spawnMock.mockReturnValueOnce(child); + + const opening = openInEditor('/tmp/solution.ts', '/tmp/leetcode'); + child.emit('spawn'); + await opening; + + expect(spawnMock).toHaveBeenCalledWith( + 'code', + ['-r', '/tmp/leetcode', '-g', '/tmp/solution.ts'], + { detached: true, stdio: 'ignore' } + ); + expect(child.unref).toHaveBeenCalledOnce(); + }); + + it('uses the configured editor before EDITOR', async () => { + const previousEditor = process.env.EDITOR; + process.env.EDITOR = 'code'; + editorMock.mockReturnValueOnce('zed'); + const child = createChildProcess(); + spawnMock.mockReturnValueOnce(child); + + const opening = openInEditor('/tmp/solution.ts'); + child.emit('spawn'); + await opening; + + expect(spawnMock).toHaveBeenCalledWith('zed', ['/tmp/solution.ts'], { + detached: true, + stdio: 'ignore', + }); + + if (previousEditor === undefined) delete process.env.EDITOR; + else process.env.EDITOR = previousEditor; + }); +}); diff --git a/src/utils/editor.ts b/src/utils/editor.ts index 47911af..3c8356f 100644 --- a/src/utils/editor.ts +++ b/src/utils/editor.ts @@ -7,6 +7,21 @@ const TERMINAL_EDITORS = ['vim', 'nvim', 'vi', 'nano', 'emacs', 'micro', 'helix' const VSCODE_EDITORS = ['code', 'code-insiders', 'cursor', 'codium', 'vscodium']; +const ZED_EDITORS = ['zed', 'zeditor', 'zed.exe']; + +function spawnDetached(editor: string, args: string[]): Promise { + return new Promise((resolve, reject) => { + const child = spawn(editor, args, { + detached: true, + stdio: 'ignore', + }); + + child.once('error', reject); + child.once('spawn', resolve); + child.unref(); + }); +} + export async function openInEditor(filePath: string, workDir?: string): Promise { const editor = config.getEditor() ?? process.env.EDITOR ?? 'code'; const workspace = workDir ?? config.getWorkDir(); @@ -28,12 +43,13 @@ export async function openInEditor(filePath: string, workDir?: string): Promise< } try { + if (ZED_EDITORS.includes(editor)) { + await spawnDetached(editor, [filePath]); + return; + } + if (VSCODE_EDITORS.includes(editor)) { - const child = spawn(editor, ['-r', workspace, '-g', filePath], { - detached: true, - stdio: 'ignore', - }); - child.unref(); + await spawnDetached(editor, ['-r', workspace, '-g', filePath]); return; } From 8ad9af6718a2d589f9ce31f059c5534770354ca8 Mon Sep 17 00:00:00 2001 From: Anurag Deo Date: Sun, 26 Jul 2026 13:29:56 +0530 Subject: [PATCH 08/14] docs(config): mention zed editor Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/commands/config.ts | 2 +- src/tui/screens/config/index.ts | 2 +- src/tui/screens/config/view.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/commands/config.ts b/src/commands/config.ts index fba3eb1..330ab49 100644 --- a/src/commands/config.ts +++ b/src/commands/config.ts @@ -128,7 +128,7 @@ export async function configInteractiveCommand(): Promise { { type: 'input', name: 'editor', - message: 'Editor command (e.g., code, vim, nvim):', + message: 'Editor command (e.g., code, zed, vim, nvim):', default: currentConfig.editor ?? 'code', }, { diff --git a/src/tui/screens/config/index.ts b/src/tui/screens/config/index.ts index 73650da..a0f886e 100644 --- a/src/tui/screens/config/index.ts +++ b/src/tui/screens/config/index.ts @@ -22,7 +22,7 @@ function buildOptions(currentConfig: ReturnType): Confi { id: 'editor', label: 'Editor Command', - description: 'Command used to open files (example: code, vim, nano)', + description: 'Command used to open files (example: code, zed, vim, nano)', value: currentConfig.editor || '', }, { diff --git a/src/tui/screens/config/view.ts b/src/tui/screens/config/view.ts index 5854af1..d1a1a08 100644 --- a/src/tui/screens/config/view.ts +++ b/src/tui/screens/config/view.ts @@ -14,7 +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', - editor: 'Example: code, vim, nvim', + editor: 'Example: code, zed, vim, nvim', workdir: 'Example: /Users/name/leetcode', repo: 'Example: https://github.com/user/leetcode.git', }; From 2d08b9ec2a46611be437460ab505efce157ae370 Mon Sep 17 00:00:00 2001 From: Anurag Deo Date: Sun, 26 Jul 2026 13:30:08 +0530 Subject: [PATCH 09/14] docs: document contest workflow Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- README.md | 17 +++++++++++++++++ docs/commands.md | 41 +++++++++++++++++++++++++++++++++++++++++ docs/config.md | 19 ++++++++++++++++++- 3 files changed, 76 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 30d479a..37ca973 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,7 @@ The CLI keeps command semantics the same and applies site-specific GraphQL queri | `snapshot ` | Save and restore solution versions | | `diff ` | Compare solution with past submissions | | `collab ` | Collaborative coding with a partner | +| `contest [slug]` | Browse and pick from contest problems | | `workspace ` | Manage workspaces for different contexts | | `config` | View or set configuration | | `sync` | Sync solutions to Git repository | @@ -313,6 +314,22 @@ leetcode collab status leetcode collab leave ``` +### Contest Problems + +```bash +# Browse contests and select a problem interactively +leetcode contest + +# Jump directly to a contest by slug +leetcode contest weekly-contest-401 + +# Override language for solution generation +leetcode contest weekly-contest-401 -l python3 + +# Skip opening in editor +leetcode contest --no-open +``` + ### Solution Snapshots ```bash diff --git a/docs/commands.md b/docs/commands.md index 6851209..d035640 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -225,6 +225,47 @@ leetcode random -d medium -t dp --pick --no-open --- +### `leetcode contest [slug]` (alias: `c`) + +Browse contests and select a problem to solve. + +**Arguments**: + +- `[slug]` - Optional contest slug for direct selection (e.g., `weekly-contest-401`) + +**Options**: + +- `-l, --lang ` - Programming language for the solution +- `--no-open` - Do not open file in editor + +**How it works**: + +- Without a slug: interactively select a contest, then select a problem by order +- With a slug: jumps directly to problem selection within that contest +- Selected problem delegates to the normal solution generation flow +- Opens the file in your configured editor (supports Zed, VS Code, Vim, etc.) + +**Examples**: + +```bash +# Browse contests and select a problem interactively +leetcode contest +leetcode c + +# Jump directly to a contest by slug +leetcode contest weekly-contest-401 +leetcode contest c weekly-contest-401 + +# Override language for solution generation +leetcode contest weekly-contest-401 -l python3 +leetcode contest -l java + +# Skip opening in editor +leetcode contest --no-open +``` + +--- + ## Solving Problems ### `leetcode pick ` (alias: `p`) diff --git a/docs/config.md b/docs/config.md index d4dc8f8..b7ef1a8 100644 --- a/docs/config.md +++ b/docs/config.md @@ -77,6 +77,9 @@ leetcode config -s leetcode.cn # Set default work directory leetcode config -w ~/Development/my-leetcode +# Use Zed to open generated solution files +leetcode config --editor zed + # Set Git repository leetcode config -r https://github.com/myuser/leetcode-solutions.git ``` @@ -88,7 +91,7 @@ Config is stored per-workspace in `~/.leetcode/workspaces//config.json`. | Key | Description | | ---------- | ---------------------------------------------------- | | `lang` | Default language extension (java, python3, sql, etc) | -| `editor` | Command to open files (code, vim, nano) | +| `editor` | Command to open files (code, zed, vim, nano) | | `workDir` | Directory where solution files are saved | | `syncRepo` | Remote Git repository URL | | `site` | LeetCode site (`leetcode.com` or `leetcode.cn`) | @@ -117,3 +120,17 @@ You can also manage workspace settings from TUI: 3. Open **Config** (`c`) to edit active workspace defaults. Both screens use buffered editing (`Enter` to save, `Esc` to cancel). + +### Zed + +Set the editor command to `zed` to open generated contest and problem files in +Zed: + +```bash +leetcode config --editor zed +``` + +`zed`, `zeditor`, and `zed.exe` are supported across platforms. Zed is launched +in the background with only the generated file path; VS Code-specific flags are +not passed to it. The configured editor takes precedence over the `EDITOR` +environment variable. From 1c91e2767e882cd389a5b16dff1393d9bbe5b069 Mon Sep 17 00:00:00 2001 From: nullptr Date: Tue, 28 Jul 2026 18:09:20 -0400 Subject: [PATCH 10/14] fix(reset): address PR review feedback --- src/__tests__/commands/reset.test.ts | 82 +++++++++++++++++++++++++++- src/commands/reset.ts | 42 ++++++++++++-- 2 files changed, 119 insertions(+), 5 deletions(-) diff --git a/src/__tests__/commands/reset.test.ts b/src/__tests__/commands/reset.test.ts index 94055ad..e7a0774 100644 --- a/src/__tests__/commands/reset.test.ts +++ b/src/__tests__/commands/reset.test.ts @@ -67,7 +67,22 @@ vi.mock('../../utils/fileUtils.js', () => ({ detectLanguageFromFile: vi.fn().mockReturnValue('typescript'), })); +vi.mock('../../storage/snapshots.js', () => ({ + snapshotStorage: { + save: vi.fn(() => ({ + id: 1, + name: 'backup-before-reset-123', + fileName: '1_backup-before-reset-123.ts', + language: 'typescript', + lines: 3, + createdAt: '2026-07-28T00:00:00.000Z', + })), + }, +})); + vi.mock('fs/promises', () => ({ + readFile: vi.fn().mockResolvedValue('existing solution'), + realpath: vi.fn(async (path: string) => path), writeFile: vi.fn().mockResolvedValue(undefined), })); @@ -85,7 +100,14 @@ vi.mock('ora', () => ({ import { resetCommand } from '../../commands/reset.js'; import { leetcodeClient } from '../../api/client.js'; import { findSolutionFile, detectLanguageFromFile } from '../../utils/fileUtils.js'; -import { writeFile } from 'fs/promises'; +import { snapshotStorage } from '../../storage/snapshots.js'; +import { readFile, realpath, writeFile } from 'fs/promises'; +import ora from 'ora'; + +function latestSpinner(): { fail: ReturnType } { + const result = vi.mocked(ora).mock.results.at(-1); + return result?.value as { fail: ReturnType }; +} describe('resetCommand', () => { beforeEach(() => { @@ -94,6 +116,16 @@ describe('resetCommand', () => { vi.mocked(leetcodeClient.getProblem).mockResolvedValue(mockProblem); vi.mocked(findSolutionFile).mockResolvedValue('/tmp/leetcode/Easy/Array/1.two-sum.ts'); vi.mocked(detectLanguageFromFile).mockReturnValue('typescript'); + vi.mocked(readFile).mockResolvedValue('existing solution'); + vi.mocked(realpath).mockImplementation(async (path) => String(path)); + vi.mocked(snapshotStorage.save).mockReturnValue({ + id: 1, + name: 'backup-before-reset-123', + fileName: '1_backup-before-reset-123.ts', + language: 'typescript', + lines: 3, + createdAt: '2026-07-28T00:00:00.000Z', + }); }); it('overwrites an existing solution file with the original stub', async () => { @@ -102,6 +134,13 @@ describe('resetCommand', () => { expect(result).toBe(true); expect(leetcodeClient.getProblemById).toHaveBeenCalledWith('1'); expect(findSolutionFile).toHaveBeenCalledWith('/tmp/leetcode', '1'); + expect(snapshotStorage.save).toHaveBeenCalledWith( + '1', + 'Two Sum', + 'existing solution', + 'typescript', + expect.stringMatching(/^backup-before-reset-\d+$/) + ); expect(writeFile).toHaveBeenCalledWith( '/tmp/leetcode/Easy/Array/1.two-sum.ts', expect.stringContaining('function twoSum(nums: number[], target: number): number[]'), @@ -126,6 +165,25 @@ describe('resetCommand', () => { expect(outputContains('Run "leetcode pick 1" first')).toBe(true); }); + it('does not write when the resolved file target is outside the workspace', async () => { + vi.mocked(realpath).mockImplementation(async (path) => { + if (path === '/tmp/leetcode/Easy/Array/1.two-sum.ts') { + return '/tmp/outside/1.two-sum.ts'; + } + return String(path); + }); + + const result = await resetCommand('1'); + + expect(result).toBe(false); + expect(readFile).not.toHaveBeenCalled(); + expect(snapshotStorage.save).not.toHaveBeenCalled(); + expect(writeFile).not.toHaveBeenCalled(); + expect(latestSpinner().fail).toHaveBeenCalledWith( + 'Security Error: File path is outside the configured workspace' + ); + }); + it('does not write when the existing file language is unsupported', async () => { vi.mocked(detectLanguageFromFile).mockReturnValueOnce(null); @@ -133,6 +191,7 @@ describe('resetCommand', () => { expect(result).toBe(false); expect(writeFile).not.toHaveBeenCalled(); + expect(latestSpinner().fail).toHaveBeenCalledWith('Unsupported file extension: .ts'); }); it('does not write when no matching template is available', async () => { @@ -144,4 +203,25 @@ describe('resetCommand', () => { expect(writeFile).not.toHaveBeenCalled(); expect(outputContains('Available languages')).toBe(true); }); + + it('does not overwrite when backup creation fails', async () => { + vi.mocked(snapshotStorage.save).mockReturnValueOnce({ error: 'Snapshot failed' }); + + const result = await resetCommand('1'); + + expect(result).toBe(false); + expect(writeFile).not.toHaveBeenCalled(); + expect(outputContains('Snapshot failed')).toBe(true); + }); + + it('prints a friendly message when the problem is not found', async () => { + vi.mocked(leetcodeClient.getProblemById).mockRejectedValueOnce( + new Error('expected object, received null') + ); + + const result = await resetCommand('9999'); + + expect(result).toBe(false); + expect(latestSpinner().fail).toHaveBeenCalledWith('Problem "9999" not found'); + }); }); diff --git a/src/commands/reset.ts b/src/commands/reset.ts index cd79a8f..24d828c 100644 --- a/src/commands/reset.ts +++ b/src/commands/reset.ts @@ -1,16 +1,26 @@ // Reset command - restore an existing solution file to the original LeetCode stub -import { writeFile } from 'fs/promises'; -import { basename } from 'path'; +import { readFile, realpath, writeFile } from 'fs/promises'; +import { basename, extname } from 'path'; import ora from 'ora'; import chalk from 'chalk'; import { leetcodeClient } from '../api/client.js'; import { requireAuth } from '../utils/auth.js'; import { config } from '../storage/config.js'; +import { snapshotStorage } from '../storage/snapshots.js'; import { findSolutionFile, detectLanguageFromFile } from '../utils/fileUtils.js'; import { generateSolutionFile, getPremiumPlaceholderCode } from '../utils/templates.js'; import { isPathInsideWorkDir } from '../utils/validation.js'; import { resolveSupportedLanguageFromLeetCodeSlug } from '../utils/languages.js'; +function isProblemNotFoundError(error: unknown): boolean { + if (!(error instanceof Error)) return false; + + return ( + error.message.includes('expected object, received null') || + /^Problem #.+ not found$/.test(error.message) + ); +} + export async function resetCommand(idOrSlug: string): Promise { const { authorized } = await requireAuth(); if (!authorized) return false; @@ -36,7 +46,9 @@ export async function resetCommand(idOrSlug: string): Promise { return false; } - if (!isPathInsideWorkDir(filePath, workDir)) { + const [realFilePath, realWorkDir] = await Promise.all([realpath(filePath), realpath(workDir)]); + + if (!isPathInsideWorkDir(realFilePath, realWorkDir)) { spinner.fail('Security Error: File path is outside the configured workspace'); console.log(chalk.gray(`File: ${filePath}`)); console.log(chalk.gray(`Workspace: ${workDir}`)); @@ -45,7 +57,7 @@ export async function resetCommand(idOrSlug: string): Promise { const language = detectLanguageFromFile(filePath); if (!language) { - spinner.fail(`Unsupported file extension: ${basename(filePath)}`); + spinner.fail(`Unsupported file extension: ${extname(filePath) || '(none)'}`); return false; } @@ -78,12 +90,34 @@ export async function resetCommand(idOrSlug: string): Promise { problem.content ?? undefined ); + const currentCode = await readFile(filePath, 'utf-8'); + const backupName = `backup-before-reset-${Date.now()}`; + const backup = snapshotStorage.save( + problem.questionFrontendId, + problem.title, + currentCode, + language, + backupName + ); + + if ('error' in backup) { + spinner.fail('Failed to create reset backup'); + console.log(chalk.red(backup.error)); + return false; + } + await writeFile(filePath, content, 'utf-8'); spinner.succeed(`Reset ${chalk.green(basename(filePath))}`); console.log(chalk.gray(`Path: ${filePath}`)); + console.log(chalk.gray(`Backup: ${backup.name}`)); return true; } catch (error) { + if (isProblemNotFoundError(error)) { + spinner.fail(`Problem "${idOrSlug}" not found`); + return false; + } + spinner.fail('Failed to reset solution'); if (error instanceof Error) { console.log(chalk.red(error.message)); From 3f58b19edfec5f04ff48a60d8c187d3717dd0dcf Mon Sep 17 00:00:00 2001 From: night-slayer18 Date: Wed, 29 Jul 2026 22:26:43 +0530 Subject: [PATCH 11/14] fix(ci): resolve auto-label 403 and security audit workflow failures - pr-check.yml: gate auto-label job to pull_request_target events only; pull_request from forks gets a read-only GITHUB_TOKEN that cannot write labels (HTTP 403). Also expand pull_request_target types to include edited/synchronize/reopened so the label is kept in sync across updates. - auto-audit.yml: use 'npm audit || true' when audit fix makes no changes so non-fixable vulnerabilities emit a warning instead of failing the job. - package.json/lock: bump @typescript-eslint/{eslint-plugin,parser} to 8.65.0 (non-breaking, compatible with eslint@9); fixes 5 of 10 high- severity vulnerabilities. Remaining 5 require eslint@10 (major bump). Also applied npm audit fix: upgraded brace-expansion, fast-uri, js-yaml, postcss and related transitive deps (12 packages total). Signed-off-by: night-slayer18 --- .github/workflows/auto-audit.yml | 4 +- .github/workflows/pr-check.yml | 5 +- package-lock.json | 237 ++++++++++++++++--------------- package.json | 4 +- 4 files changed, 134 insertions(+), 116 deletions(-) diff --git a/.github/workflows/auto-audit.yml b/.github/workflows/auto-audit.yml index cc08933..68bee4c 100644 --- a/.github/workflows/auto-audit.yml +++ b/.github/workflows/auto-audit.yml @@ -100,7 +100,9 @@ jobs: run: | echo "Vulnerabilities were found but npm audit fix made no changes." echo "Some vulnerabilities may require a manual semver-major update." - npm audit + # Use || true so non-fixable vulns emit a visible warning without + # failing the job (they require a manual semver-major update). + npm audit || true # โ”€โ”€ Commit, push, PR to dev, then PR to main โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - name: Commit fix and open PR to dev diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 97ae68b..7c7bd90 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -6,7 +6,7 @@ on: types: [opened, edited, synchronize, reopened] pull_request_target: branches: [main, dev] - types: [opened] + types: [opened, edited, synchronize, reopened] permissions: pull-requests: write @@ -61,6 +61,9 @@ jobs: auto-label: name: Auto Label runs-on: ubuntu-latest + # Only run under pull_request_target which has a write-access GITHUB_TOKEN. + # pull_request from forks gets a read-only token โ†’ 403 on label API. + if: github.event_name == 'pull_request_target' steps: - name: Apply label based on PR title prefix uses: actions/github-script@v9 diff --git a/package-lock.json b/package-lock.json index 0d2c17d..7f7fc90 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,8 +35,8 @@ "@types/inquirer": "^9.0.7", "@types/node": "^22.10.2", "@types/striptags": "^0.0.5", - "@typescript-eslint/eslint-plugin": "^8.53.0", - "@typescript-eslint/parser": "^8.53.0", + "@typescript-eslint/eslint-plugin": "^8.65.0", + "@typescript-eslint/parser": "^8.65.0", "@vitest/coverage-v8": "^4.0.16", "eslint": "^9.39.2", "prettier": "^3.8.0", @@ -45,7 +45,7 @@ "vitest": "^4.0.16" }, "engines": { - "node": ">=20.0.0" + "node": ">=22.0.0" } }, "node_modules/@babel/helper-string-parser": { @@ -639,9 +639,9 @@ } }, "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "version": "1.1.17", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.17.tgz", + "integrity": "sha512-w+aeW/mkgM4PyRMOJCgi3fOrTm5Q8QY1OSfn2TO2iuDj3ezIHqejmuxbjfPrqUkgqRew1iqkyAn0tr0ZwHD9+w==", "dev": true, "license": "MIT", "dependencies": { @@ -730,9 +730,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "version": "1.1.17", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.17.tgz", + "integrity": "sha512-w+aeW/mkgM4PyRMOJCgi3fOrTm5Q8QY1OSfn2TO2iuDj3ezIHqejmuxbjfPrqUkgqRew1iqkyAn0tr0ZwHD9+w==", "dev": true, "license": "MIT", "dependencies": { @@ -2175,20 +2175,20 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.54.0.tgz", - "integrity": "sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.65.0.tgz", + "integrity": "sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.54.0", - "@typescript-eslint/type-utils": "8.54.0", - "@typescript-eslint/utils": "8.54.0", - "@typescript-eslint/visitor-keys": "8.54.0", + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/type-utils": "8.65.0", + "@typescript-eslint/utils": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.4.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2198,22 +2198,22 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.54.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "@typescript-eslint/parser": "^8.65.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.54.0.tgz", - "integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.65.0.tgz", + "integrity": "sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.54.0", - "@typescript-eslint/types": "8.54.0", - "@typescript-eslint/typescript-estree": "8.54.0", - "@typescript-eslint/visitor-keys": "8.54.0", + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", "debug": "^4.4.3" }, "engines": { @@ -2224,19 +2224,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.54.0.tgz", - "integrity": "sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.65.0.tgz", + "integrity": "sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.54.0", - "@typescript-eslint/types": "^8.54.0", + "@typescript-eslint/tsconfig-utils": "^8.65.0", + "@typescript-eslint/types": "^8.65.0", "debug": "^4.4.3" }, "engines": { @@ -2247,18 +2247,18 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.54.0.tgz", - "integrity": "sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.65.0.tgz", + "integrity": "sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.54.0", - "@typescript-eslint/visitor-keys": "8.54.0" + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2269,9 +2269,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.54.0.tgz", - "integrity": "sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.65.0.tgz", + "integrity": "sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==", "dev": true, "license": "MIT", "engines": { @@ -2282,21 +2282,21 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.54.0.tgz", - "integrity": "sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.65.0.tgz", + "integrity": "sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.54.0", - "@typescript-eslint/typescript-estree": "8.54.0", - "@typescript-eslint/utils": "8.54.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0", + "@typescript-eslint/utils": "8.65.0", "debug": "^4.4.3", - "ts-api-utils": "^2.4.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2306,14 +2306,14 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.54.0.tgz", - "integrity": "sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz", + "integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==", "dev": true, "license": "MIT", "engines": { @@ -2325,21 +2325,21 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.54.0.tgz", - "integrity": "sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.65.0.tgz", + "integrity": "sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.54.0", - "@typescript-eslint/tsconfig-utils": "8.54.0", - "@typescript-eslint/types": "8.54.0", - "@typescript-eslint/visitor-keys": "8.54.0", + "@typescript-eslint/project-service": "8.65.0", + "@typescript-eslint/tsconfig-utils": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", "debug": "^4.4.3", - "minimatch": "^9.0.5", + "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.4.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2349,20 +2349,20 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/utils": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.54.0.tgz", - "integrity": "sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.65.0.tgz", + "integrity": "sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.54.0", - "@typescript-eslint/types": "8.54.0", - "@typescript-eslint/typescript-estree": "8.54.0" + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2372,19 +2372,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.54.0.tgz", - "integrity": "sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA==", + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.65.0.tgz", + "integrity": "sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.54.0", - "eslint-visitor-keys": "^4.2.1" + "@typescript-eslint/types": "8.65.0", + "eslint-visitor-keys": "^5.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2395,13 +2395,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" @@ -2718,13 +2718,26 @@ } }, "node_modules/brace-expansion": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", - "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz", + "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/brace-expansion/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" } }, "node_modules/buffer": { @@ -3549,9 +3562,9 @@ } }, "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "version": "1.1.17", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.17.tgz", + "integrity": "sha512-w+aeW/mkgM4PyRMOJCgi3fOrTm5Q8QY1OSfn2TO2iuDj3ezIHqejmuxbjfPrqUkgqRew1iqkyAn0tr0ZwHD9+w==", "dev": true, "license": "MIT", "dependencies": { @@ -3746,9 +3759,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", - "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz", + "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==", "funding": [ { "type": "github", @@ -4323,9 +4336,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", - "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", "dev": true, "funding": [ { @@ -4868,16 +4881,16 @@ } }, "node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^2.0.2" + "brace-expansion": "^5.0.8" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -4939,9 +4952,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.15", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", - "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", "dev": true, "funding": [ { @@ -5300,9 +5313,9 @@ } }, "node_modules/postcss": { - "version": "8.5.15", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", - "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "version": "8.5.25", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz", + "integrity": "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==", "dev": true, "funding": [ { @@ -5320,7 +5333,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -6168,9 +6181,9 @@ } }, "node_modules/ts-api-utils": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", - "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", "dev": true, "license": "MIT", "engines": { diff --git a/package.json b/package.json index aadab30..08d67a2 100644 --- a/package.json +++ b/package.json @@ -63,8 +63,8 @@ "@types/inquirer": "^9.0.7", "@types/node": "^22.10.2", "@types/striptags": "^0.0.5", - "@typescript-eslint/eslint-plugin": "^8.53.0", - "@typescript-eslint/parser": "^8.53.0", + "@typescript-eslint/eslint-plugin": "^8.65.0", + "@typescript-eslint/parser": "^8.65.0", "@vitest/coverage-v8": "^4.0.16", "eslint": "^9.39.2", "prettier": "^3.8.0", From 8af0a58be4de5fa9ee61a910c0be8686729a8d94 Mon Sep 17 00:00:00 2001 From: night-slayer18 Date: Wed, 29 Jul 2026 22:29:50 +0530 Subject: [PATCH 12/14] chore(deps): upgrade eslint to v10 and typescript-eslint to 8.65.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves all 10 high-severity vulnerabilities from npm audit: - eslint 9 โ†’ 10.8.0: fixes brace-expansion, minimatch, @eslint/config-array, @eslint/eslintrc vulnerabilities (chain rooted in brace-expansion <= 5.0.7). - @typescript-eslint/{eslint-plugin,parser} 8.54.0 โ†’ 8.65.0: fixes typescript-estree, type-utils, utils vulnerabilities. The project already uses ESLint flat config (eslint.config.js) so the eslint@10 breaking change (dropping .eslintrc.* support) has no impact. All 305 tests pass, lint: 0 errors, build: clean. Signed-off-by: night-slayer18 --- package-lock.json | 441 ++++++++-------------------------------------- package.json | 2 +- 2 files changed, 74 insertions(+), 369 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7f7fc90..dbcf695 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,7 +38,7 @@ "@typescript-eslint/eslint-plugin": "^8.65.0", "@typescript-eslint/parser": "^8.65.0", "@vitest/coverage-v8": "^4.0.16", - "eslint": "^9.39.2", + "eslint": "^10.8.0", "prettier": "^3.8.0", "tsup": "^8.3.5", "typescript": "^5.7.2", @@ -624,187 +624,68 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", - "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.7", + "@eslint/object-schema": "^3.0.5", "debug": "^4.3.1", - "minimatch": "^3.1.2" + "minimatch": "^10.2.4" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "1.1.17", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.17.tgz", - "integrity": "sha512-w+aeW/mkgM4PyRMOJCgi3fOrTm5Q8QY1OSfn2TO2iuDj3ezIHqejmuxbjfPrqUkgqRew1iqkyAn0tr0ZwHD9+w==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/config-array/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/config-helpers": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", - "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", + "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.17.0" + "@eslint/core": "^1.2.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/core": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", "dev": true, "license": "Apache-2.0", "dependencies": { "@types/json-schema": "^7.0.15" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", - "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.1", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.17", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.17.tgz", - "integrity": "sha512-w+aeW/mkgM4PyRMOJCgi3fOrTm5Q8QY1OSfn2TO2iuDj3ezIHqejmuxbjfPrqUkgqRew1iqkyAn0tr0ZwHD9+w==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/eslintrc/node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@eslint/js": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", - "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/object-schema": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", - "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/plugin-kit": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", - "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", + "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.17.0", + "@eslint/core": "^1.2.1", "levn": "^0.4.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@humanfs/core": { @@ -2102,6 +1983,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -2552,9 +2440,9 @@ } }, "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", "dev": true, "license": "MIT", "bin": { @@ -2640,13 +2528,6 @@ "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", "license": "MIT" }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -2679,13 +2560,6 @@ "when-exit": "^2.1.4" } }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -2853,16 +2727,6 @@ "@keyv/serialize": "^1.1.1" } }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/chai": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", @@ -3116,13 +2980,6 @@ "node": ">=18" } }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, "node_modules/conf": { "version": "13.1.0", "resolved": "https://registry.npmjs.org/conf/-/conf-13.1.0.tgz", @@ -3439,33 +3296,33 @@ } }, "node_modules/eslint": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", - "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "version": "10.8.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.8.0.tgz", + "integrity": "sha512-nuKKvN+oIBO0koN7Tm7dlkmnkc21mtt0QJLwAKzjLq14y6lRTdVG36MZHJ8eQHwdJMwZbQNMlPOYedMq/oVJvQ==", "dev": true, "license": "MIT", + "workspaces": [ + "packages/*" + ], "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.1", - "@eslint/config-helpers": "^0.4.2", - "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.2", - "@eslint/plugin-kit": "^0.4.1", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.7.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.2", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", - "ajv": "^6.12.4", - "chalk": "^4.0.0", + "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", @@ -3475,8 +3332,7 @@ "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", + "minimatch": "^10.2.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, @@ -3484,7 +3340,7 @@ "eslint": "bin/eslint.js" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://eslint.org/donate" @@ -3499,17 +3355,19 @@ } }, "node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" @@ -3545,58 +3403,14 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/eslint/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.17", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.17.tgz", - "integrity": "sha512-w+aeW/mkgM4PyRMOJCgi3fOrTm5Q8QY1OSfn2TO2iuDj3ezIHqejmuxbjfPrqUkgqRew1iqkyAn0tr0ZwHD9+w==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" @@ -3619,45 +3433,32 @@ "dev": true, "license": "MIT" }, - "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.15.0", + "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" + "eslint-visitor-keys": "^5.0.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" @@ -3941,19 +3742,6 @@ "node": ">=10.13.0" } }, - "node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/got": { "version": "14.6.6", "resolved": "https://registry.npmjs.org/got/-/got-14.6.6.tgz", @@ -4088,23 +3876,6 @@ "node": ">= 4" } }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -4335,29 +4106,6 @@ "dev": true, "license": "MIT" }, - "node_modules/js-yaml": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", - "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/puzrin" - }, - { - "type": "github", - "url": "https://github.com/sponsors/nodeca" - } - ], - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -4738,13 +4486,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, "node_modules/log-symbols": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", @@ -5209,19 +4950,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/parse5": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", @@ -5557,16 +5285,6 @@ "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", "license": "MIT" }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/responselike": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/responselike/-/responselike-4.0.2.tgz", @@ -5985,19 +5703,6 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/striptags": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/striptags/-/striptags-3.2.0.tgz", diff --git a/package.json b/package.json index 08d67a2..4b5710f 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "@typescript-eslint/eslint-plugin": "^8.65.0", "@typescript-eslint/parser": "^8.65.0", "@vitest/coverage-v8": "^4.0.16", - "eslint": "^9.39.2", + "eslint": "^10.8.0", "prettier": "^3.8.0", "tsup": "^8.3.5", "typescript": "^5.7.2", From 4ec16de329849ef9354b30d926afa9f511afc7dd Mon Sep 17 00:00:00 2001 From: night-slayer18 Date: Wed, 29 Jul 2026 22:44:28 +0530 Subject: [PATCH 13/14] docs: add reset command section under usage examples in README Signed-off-by: night-slayer18 --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index 80f85b7..2d7e41a 100644 --- a/README.md +++ b/README.md @@ -344,6 +344,16 @@ leetcode contest weekly-contest-401 -l python3 leetcode contest --no-open ``` +### Reset Solution + +```bash +# Reset a solution file back to the original LeetCode stub (by problem ID) +leetcode reset 1 + +# Reset by problem slug +leetcode reset two-sum +``` + ### Solution Snapshots ```bash From 94374d0f46ae7f59e673e1b18c3b8074978798c0 Mon Sep 17 00:00:00 2001 From: night-slayer18 Date: Wed, 29 Jul 2026 23:13:02 +0530 Subject: [PATCH 14/14] chore(release): bump version to 3.4.0 and update release notes Signed-off-by: night-slayer18 --- docs/releases.md | 50 +++++++++++++++++++++++++++++++++++++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- src/index.ts | 2 +- 4 files changed, 54 insertions(+), 4 deletions(-) diff --git a/docs/releases.md b/docs/releases.md index 4852a01..6e83674 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -1,5 +1,55 @@ # Release Notes +## v3.4.0 + +> **Release Date**: 2026-07-29 +> **Focus**: Contest Navigation + Solution Reset + Zed Editor Support + Security & CI Overhaul + +### ๐Ÿš€ Features + +#### Contest Navigation (`leetcode contest [slug]`) + +Browse available LeetCode contests and pick contest problems interactively or directly by contest slug. + +- Browse global (`leetcode.com`) and China (`leetcode.cn`) contests +- Select contest problems in 1-indexed contest order +- Fully integrated with language selection (`-l/--lang`) and editor launching +- Added shell completions for Fish and Zsh + +```bash +leetcode contest # Browse contests interactively +leetcode contest weekly-contest-401 # Open contest by slug directly +``` + +#### Solution Reset (`leetcode reset `) + +Restores an existing solution file back to the original LeetCode code stub. + +- Automatically detects solution language from file extension +- Safety snapshot backup (`backup-before-reset-`) created before overwriting +- Validates workspace boundaries to prevent unintended file modifications + +```bash +leetcode reset 1 # Reset problem 1 solution +leetcode reset two-sum # Reset by problem slug +``` + +#### Zed Editor Support + +Full support for Zed editor (`zed`, `zeditor`, `zed.exe`) across macOS, Linux, and Windows. + +```bash +leetcode config --editor zed +``` + +### ๐Ÿ”’ Security & Maintenance + +- Upgraded `eslint` to `v10.8.0` and `@typescript-eslint` packages to `8.65.0`. +- Fixed all 10 high-severity security vulnerabilities reported by `npm audit` (0 vulnerabilities remaining). +- Fixed `auto-label` permission issue on fork PRs (`pull_request_target` event). + +--- + ## v3.3.0 > **Release Date**: 2026-07-07 diff --git a/package-lock.json b/package-lock.json index dbcf695..5cf15bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@night-slayer18/leetcode-cli", - "version": "3.3.0", + "version": "3.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@night-slayer18/leetcode-cli", - "version": "3.3.0", + "version": "3.4.0", "license": "Apache-2.0", "dependencies": { "@supabase/supabase-js": "^2.90.1", diff --git a/package.json b/package.json index 4b5710f..c569fa0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@night-slayer18/leetcode-cli", - "version": "3.3.0", + "version": "3.4.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 4a75c32..c814df1 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.3.0', '-v, --version', 'Output the version number') + .version('3.4.0', '-v, --version', 'Output the version number') .helpOption('-h, --help', 'Display help for command') .addHelpText( 'after',