Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
b855ad8
feat: add LeetCodeSite type and site utility helpers
night-slayer18 Apr 20, 2026
a5aec84
feat: split GraphQL queries into site-specific packs
night-slayer18 Apr 20, 2026
c47f8c6
feat: add Zod schemas for leetcode.cn API responses
night-slayer18 Apr 20, 2026
ecceb06
feat: add CN response adapters for data normalization
night-slayer18 Apr 20, 2026
09822e4
feat: make LeetCodeClient site-aware
night-slayer18 Apr 20, 2026
3f5e15c
feat: persist site selection in workspace config
night-slayer18 Apr 20, 2026
4e9cb7c
feat: configure client site from workspace config on auth
night-slayer18 Apr 20, 2026
ee94495
feat: add site selection to login, config, and workspace commands
night-slayer18 Apr 20, 2026
46ecab5
feat: add site support to TUI config and effects
night-slayer18 Apr 20, 2026
8d8bc72
test: add tests for CN adapters, query resolver, and site-aware mocks
night-slayer18 Apr 20, 2026
d85689a
docs: document leetcode.cn site support
night-slayer18 Apr 20, 2026
84f91ba
ci: run CI and CodeQL workflows on dev branch
night-slayer18 Apr 20, 2026
f9e6d4c
feat: add support for leetcode.cn in tui
dong-frank Apr 21, 2026
de623e0
feat: add support for 'list' and other commands for leetcode.cn
dong-frank Apr 21, 2026
46893c4
Merge pull request #10 from dong-frank/feat/leetcode.cn
night-slayer18 Apr 21, 2026
c22ff26
fix: ensure credential clearance on site switch & improve CLI test sa…
night-slayer18 Apr 21, 2026
286dd03
fix(docs/tui): add auth backend details to CLI help & force active se…
night-slayer18 Apr 21, 2026
3552caf
chore(release): prepare v3.1.0
night-slayer18 May 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ name: CI

on:
push:
branches: [main]
branches: [main, dev]
pull_request:
branches: [main]
branches: [main, dev]

jobs:
build:
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ name: CodeQL

on:
push:
branches: [main]
branches: [main, dev]
pull_request:
branches: [main]
branches: [main, dev]
schedule:
- cron: '0 0 * * 1' # Weekly on Monday

Expand Down
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ A modern, feature-rich LeetCode CLI built with TypeScript.
- 📸 **Solution snapshots** - Save, restore, and compare solution versions
- 👥 **Collaborative coding** - Solve problems with a partner
- 📁 **Workspaces** - Isolate contexts (interview prep, study, contests)
- ⚙️ **Configurable** - Set language, editor, and working directory
- ⚙️ **Configurable** - Set language, editor, working directory, and LeetCode site
- 🖥️ **Interactive TUI** - Launch full-screen terminal workflow with `leetcode`
- 📂 **Smart file discovery** - Use problem ID, filename, or full path
- 🔄 **Git Sync** - Auto-sync solutions to GitHub/GitLab
Expand Down Expand Up @@ -54,6 +54,9 @@ leetcode
# Login with your LeetCode cookies
leetcode login

# Optional: switch to LeetCode China
leetcode config --site leetcode.cn

# Get today's daily challenge
leetcode daily

Expand All @@ -79,6 +82,19 @@ Run `leetcode` with no arguments to open the full-screen TUI.

See [docs/tui.md](docs/tui.md) for full keybindings and behavior.

## Site Support

- Default site: `leetcode.com`
- Optional site: `leetcode.cn`
- Set site with:

```bash
leetcode config --site leetcode.com
leetcode config --site leetcode.cn
```

The CLI keeps command semantics the same and applies site-specific GraphQL queries/adapters internally.

## Commands

| Command | Description |
Expand Down
9 changes: 8 additions & 1 deletion docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ See [TUI Guide](tui.md) for complete behavior and screen-specific shortcuts.
Login to LeetCode with browser cookies.

Notes:
- `leetcode login` lets you choose site (`leetcode.com` or `leetcode.cn`) before cookie input.
- Default credential backend is system keychain.
- Set `LEETCODECLI_CREDENTIAL_BACKEND=file` with `LEETCODECLI_MASTER_KEY` for encrypted file mode.
- If both `LEETCODE_SESSION` and `LEETCODE_CSRF_TOKEN` are set, login runs in read-only env mode.
Expand Down Expand Up @@ -499,9 +500,10 @@ View or set configuration.
**Options**:

- `-l, --lang <language>` - Set default programming language
- `-s, --site <site>` - Set LeetCode site (`leetcode.com` or `leetcode.cn`)
- `-e, --editor <editor>` - Set editor command
- `-w, --workdir <path>` - Set working directory for solutions
- `-r, --repo <url>` - Set Git repository URL
- `-r, --repo [url]` - Set or clear Git repository URL
- `-i, --interactive` - Interactive configuration mode

**Examples**:
Expand All @@ -515,6 +517,10 @@ leetcode config -l python3
leetcode config --lang java
leetcode config --lang sql

# Set site
leetcode config -s leetcode.com
leetcode config --site leetcode.cn

# Set editor
leetcode config -e code
leetcode config --editor vim
Expand All @@ -525,6 +531,7 @@ leetcode config --workdir /Users/you/projects/leetcode

# Set Git repository
leetcode config -r https://github.com/user/repo.git
leetcode config --repo

# Interactive configuration
leetcode config -i
Expand Down
16 changes: 11 additions & 5 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@

The CLI requires your LeetCode authentication cookies.

1. Login to [leetcode.com](https://leetcode.com).
2. Open Browser DevTools (F12) -> Application -> Cookies.
3. Find `LEETCODE_SESSION` and `csrftoken`.
4. Run:
1. Choose your site (`leetcode.com` or `leetcode.cn`).
2. Login to that site in your browser.
3. Open Browser DevTools (F12) -> Application -> Cookies.
4. Find `LEETCODE_SESSION` and `csrftoken`.
5. Run:
```bash
leetcode login
```
5. Paste the values.
6. Paste the values.

## Credential Storage

Expand Down Expand Up @@ -69,6 +70,10 @@ leetcode config -l python3
# Set SQL as default language
leetcode config -l sql

# Select LeetCode site
leetcode config -s leetcode.com
leetcode config -s leetcode.cn

# Set default work directory
leetcode config -w ~/Development/my-leetcode

Expand All @@ -86,6 +91,7 @@ Config is stored per-workspace in `~/.leetcode/workspaces/<name>/config.json`.
| `editor` | Command to open files (code, vim, nano) |
| `workDir` | Directory where solution files are saved |
| `syncRepo` | Remote Git repository URL |
| `site` | LeetCode site (`leetcode.com` or `leetcode.cn`) |

## Workspace-Aware Storage

Expand Down
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ A modern, feature-rich command-line interface for LeetCode, built with TypeScrip
## Features

- 🔐 Cookie-based authentication
- 🌐 Site-aware API support (`leetcode.com` and `leetcode.cn`)
- 📝 Auto-generation of solution files
- 🧪 Local testing against sample cases
- 📤 Direct submission to LeetCode
Expand Down
33 changes: 33 additions & 0 deletions docs/releases.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,38 @@
# Release Notes

## v3.1.0

> **Release Date**: 2026-05-01
> **Focus**: LeetCode China (leetcode.cn) Full Support + Credential UX

### 🌏 LeetCode China Support

- Full `leetcode.cn` integration across CLI and TUI.
- New `LeetCodeSite` type with site utility helpers (`normalizeLeetCodeSiteInput`, `getLeetCodeSiteLabel`).
- GraphQL query packs split into site-specific files (`queries.global.ts`, `queries.cn.ts`) — CN uses the correct native schema for problem list, daily, and problem detail.
- CN-specific Zod schemas for type-safe response parsing.
- CN response adapters that normalize China API responses into the shared CLI data model.
- `LeetCodeClient` is now fully site-aware: switches base URL, query pack, and cookie headers automatically.
- Site selection available in `login`, `config`, and `workspace` commands.
- Site preference persisted per workspace in config file.
- TUI Config screen: site switching with a mandatory confirmation modal that forces an immediate session logout and redirect to login.

### 🔐 Credential & Auth UX

- `leetcode login --help` now documents all three credential storage backends:
1. **System Keychain** (default) — macOS/Windows/Linux OS-native secure storage via `keytar`.
2. **Encrypted File** — AES-256-GCM via `LEETCODECLI_CREDENTIAL_BACKEND=file` + `LEETCODECLI_MASTER_KEY`.
3. **Environment Variables** — read-only headless mode via `LEETCODE_SESSION` + `LEETCODE_CSRF_TOKEN`.
- TUI site switch now correctly wipes the in-memory session (not just persisted config), ensuring users are immediately signed out and redirected to the login screen.

### 🧪 Test Stability

- Mocked `versionStorage` in update/changelog tests to prevent phantom `999.0.0` update cache leakage into real environments.
- Mocked outbound `got` network requests in changelog tests to prevent rate-limit failures in CI.
- All 283 tests pass cleanly across Node 20/22/24 on Ubuntu and macOS.

---

## v3.0.1

> **Release Date**: 2026-04-20
Expand Down
1 change: 1 addition & 0 deletions docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ Run the actual compiled CLI binary to catch:
Deterministic CLI command-flow checks run in the CI OS matrix (Linux/macOS/Windows):

- Config set/read with SQL language and path values
- Config set/read with site selection (`leetcode.com` / `leetcode.cn`)
- Workspace create/use/list/current command flows
- Snapshot save/list/diff flow on real SQL files
- Built CLI help output includes SQL language support
Expand Down
10 changes: 5 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@night-slayer18/leetcode-cli",
"version": "3.0.1",
"version": "3.1.0",
"description": "A modern LeetCode CLI built with TypeScript",
"type": "module",
"main": "dist/index.js",
Expand Down
140 changes: 140 additions & 0 deletions src/__tests__/api/client-cn.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { describe, expect, it, vi } from 'vitest';
import { LeetCodeClient } from '../../api/client.js';

describe('LeetCodeClient cn getProblemById', () => {
it('uses list search to resolve cn problem ids by exact frontend id', async () => {
const client = new LeetCodeClient('leetcode.cn');

const getProblemsSpy = vi.spyOn(client, 'getProblems').mockResolvedValueOnce({
total: 1,
problems: [
{
questionId: '1',
questionFrontendId: '1',
title: '两数之和',
titleSlug: 'two-sum',
difficulty: 'Easy',
isPaidOnly: false,
acRate: 52.3,
topicTags: [],
status: 'ac',
},
],
});

const getProblemSpy = vi.spyOn(client, 'getProblem').mockResolvedValue({
questionId: '1',
questionFrontendId: '1',
title: '两数之和',
titleSlug: 'two-sum',
difficulty: 'Easy',
isPaidOnly: false,
acRate: 52.3,
topicTags: [],
status: 'ac',
content: '<p>Given an array of integers...</p>',
codeSnippets: [],
sampleTestCase: '',
exampleTestcases: '',
hints: [],
companyTags: [],
stats: '{}',
});

const result = await client.getProblemById('1');

expect(getProblemsSpy).toHaveBeenCalledWith({ searchKeywords: '1', limit: 50, skip: 0 });
expect(getProblemSpy).toHaveBeenCalledWith('two-sum');
expect(result.titleSlug).toBe('two-sum');
});

it('throws when cn list search does not contain the exact frontend id', async () => {
const client = new LeetCodeClient('leetcode.cn');

const getProblemsSpy = vi.spyOn(client, 'getProblems').mockResolvedValueOnce({
total: 12,
problems: [
{
questionId: '200',
questionFrontendId: '200',
title: '岛屿数量',
titleSlug: 'number-of-islands',
difficulty: 'Medium',
isPaidOnly: false,
acRate: 61.2,
topicTags: [],
status: null,
},
],
});

const getProblemSpy = vi.spyOn(client, 'getProblem');

await expect(client.getProblemById('2')).rejects.toThrow('Problem #2 not found');

expect(getProblemsSpy).toHaveBeenCalledWith({ searchKeywords: '2', limit: 50, skip: 0 });
expect(getProblemSpy).not.toHaveBeenCalled();
});

it('searches additional cn result pages until the exact frontend id is found', async () => {
const client = new LeetCodeClient('leetcode.cn');

const getProblemsSpy = vi
.spyOn(client, 'getProblems')
.mockResolvedValueOnce({
total: 120,
problems: Array.from({ length: 50 }, (_, index) => ({
questionId: String(index + 100),
questionFrontendId: String(index + 100),
title: `Problem ${index + 100}`,
titleSlug: `problem-${index + 100}`,
difficulty: 'Easy' as const,
isPaidOnly: false,
acRate: 50,
topicTags: [],
status: null,
})),
})
.mockResolvedValueOnce({
total: 120,
problems: [
{
questionId: '1',
questionFrontendId: '1',
title: '两数之和',
titleSlug: 'two-sum',
difficulty: 'Easy',
isPaidOnly: false,
acRate: 52.3,
topicTags: [],
status: 'ac',
},
],
});

const getProblemSpy = vi.spyOn(client, 'getProblem').mockResolvedValue({
questionId: '1',
questionFrontendId: '1',
title: '两数之和',
titleSlug: 'two-sum',
difficulty: 'Easy',
isPaidOnly: false,
acRate: 52.3,
topicTags: [],
status: 'ac',
content: '<p>Given an array of integers...</p>',
codeSnippets: [],
sampleTestCase: '',
exampleTestcases: '',
hints: [],
companyTags: [],
stats: '{}',
});

await client.getProblemById('1');

expect(getProblemsSpy).toHaveBeenNthCalledWith(1, { searchKeywords: '1', limit: 50, skip: 0 });
expect(getProblemsSpy).toHaveBeenNthCalledWith(2, { searchKeywords: '1', limit: 50, skip: 50 });
expect(getProblemSpy).toHaveBeenCalledWith('two-sum');
});
});
Loading
Loading