@night-slayer18/leetcode-cli 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -117,6 +117,42 @@ leetcode submit 20.valid-parentheses.java
117
117
 
118
118
  # Using full path
119
119
  leetcode test ./Easy/String/20.valid-parentheses.java
120
+
121
+ # With custom test case
122
+ leetcode test 20 -c "[1,2,3]\n4"
123
+ ```
124
+
125
+ ### Random Problem
126
+
127
+ Fetch and solve a random problem.
128
+
129
+ ```bash
130
+ # Get random problem
131
+ leetcode random
132
+
133
+ # Filter by difficulty
134
+ leetcode random -d hard
135
+
136
+ # Filter by topic tag
137
+ leetcode random -t dp
138
+
139
+ # Pick immediately
140
+ leetcode random -d medium --pick
141
+ ```
142
+
143
+ ### View & Download Submissions
144
+
145
+ View past submissions and download code.
146
+
147
+ ```bash
148
+ # List last 20 submissions
149
+ leetcode submissions 1
150
+
151
+ # View details of last accepted submission
152
+ leetcode submissions 1 --last
153
+
154
+ # Download last accepted solution
155
+ leetcode submissions 1 --download
120
156
  ```
121
157
 
122
158
  ### Configuration
@@ -200,6 +236,44 @@ Config is stored at `~/.leetcode/config.json`:
200
236
 
201
237
  - Node.js >= 20.0.0
202
238
 
239
+ ## Docker Usage
240
+
241
+ You can run the CLI using Docker without installing Node.js.
242
+
243
+ ### Method 1: Pre-built Image (Recommended)
244
+
245
+ 1. **Pull the image**:
246
+ ```bash
247
+ docker pull nightslayer/leetcode-cli:latest
248
+ ```
249
+
250
+ 2. **Setup Alias** (Add to your `~/.zshrc` or `~/.bashrc`):
251
+ ```bash
252
+ alias leetcode="docker run -it --rm -v \$(pwd)/leetcode:/root/leetcode -v ~/.leetcode:/root/.leetcode nightslayer/leetcode-cli:latest"
253
+ ```
254
+
255
+ 3. **Usage**:
256
+ ```bash
257
+ leetcode list
258
+ ```
259
+
260
+ ### Method 2: Build Locally
261
+
262
+ 1. **Build the image**:
263
+ ```bash
264
+ docker build -t leetcode-cli .
265
+ ```
266
+
267
+ 2. **Run commands**:
268
+ ```bash
269
+ # Run 'list' command
270
+ docker run -it --rm \
271
+ -v $(pwd)/leetcode:/root/leetcode \
272
+ -v ~/.leetcode:/root/.leetcode \
273
+ leetcode-cli list
274
+ ```
275
+ *Note: We mount `~/.leetcode` to persist login credentials and `leetcode` folder to save solution files.*
276
+
203
277
  ## License
204
278
 
205
279
  Apache-2.0 © [night-slayer18](https://github.com/night-slayer18)
package/dist/index.d.ts CHANGED
@@ -1 +1,2 @@
1
- #!/usr/bin/env node
1
+
2
+ export { }
package/dist/index.js CHANGED
@@ -1,8 +1,6 @@
1
- #!/usr/bin/env node
2
-
3
1
  // src/index.ts
4
2
  import { Command } from "commander";
5
- import chalk11 from "chalk";
3
+ import chalk13 from "chalk";
6
4
 
7
5
  // src/commands/login.ts
8
6
  import inquirer from "inquirer";
@@ -121,6 +119,41 @@ var DAILY_CHALLENGE_QUERY = `
121
119
  }
122
120
  }
123
121
  `;
122
+ var SUBMISSION_LIST_QUERY = `
123
+ query submissionList($questionSlug: String!, $limit: Int, $offset: Int) {
124
+ questionSubmissionList(
125
+ questionSlug: $questionSlug
126
+ limit: $limit
127
+ offset: $offset
128
+ ) {
129
+ submissions {
130
+ id
131
+ statusDisplay
132
+ lang
133
+ runtime
134
+ timestamp
135
+ memory
136
+ }
137
+ }
138
+ }
139
+ `;
140
+ var RANDOM_PROBLEM_QUERY = `
141
+ query randomQuestion($categorySlug: String, $filters: QuestionListFilterInput) {
142
+ randomQuestion(categorySlug: $categorySlug, filters: $filters) {
143
+ titleSlug
144
+ }
145
+ }
146
+ `;
147
+ var SUBMISSION_DETAILS_QUERY = `
148
+ query submissionDetails($submissionId: Int!) {
149
+ submissionDetails(submissionId: $submissionId) {
150
+ code
151
+ lang {
152
+ name
153
+ }
154
+ }
155
+ }
156
+ `;
124
157
 
125
158
  // src/api/client.ts
126
159
  var LEETCODE_BASE_URL = "https://leetcode.com";
@@ -210,6 +243,20 @@ var LeetCodeClient = class {
210
243
  const data = await this.graphql(DAILY_CHALLENGE_QUERY);
211
244
  return data.activeDailyCodingChallengeQuestion;
212
245
  }
246
+ async getRandomProblem(filters = {}) {
247
+ const variables = {
248
+ categorySlug: "",
249
+ filters: {}
250
+ };
251
+ if (filters.difficulty) {
252
+ variables.filters.difficulty = filters.difficulty;
253
+ }
254
+ if (filters.tags?.length) {
255
+ variables.filters.tags = filters.tags;
256
+ }
257
+ const data = await this.graphql(RANDOM_PROBLEM_QUERY, variables);
258
+ return data.randomQuestion.titleSlug;
259
+ }
213
260
  async getUserProfile(username) {
214
261
  const data = await this.graphql(USER_PROFILE_QUERY, { username });
215
262
  const user = data.matchedUser;
@@ -222,6 +269,14 @@ var LeetCodeClient = class {
222
269
  totalActiveDays: user.userCalendar.totalActiveDays
223
270
  };
224
271
  }
272
+ async getSubmissionList(slug, limit = 20, offset = 0) {
273
+ const data = await this.graphql(SUBMISSION_LIST_QUERY, { questionSlug: slug, limit, offset });
274
+ return data.questionSubmissionList.submissions;
275
+ }
276
+ async getSubmissionDetails(submissionId) {
277
+ const data = await this.graphql(SUBMISSION_DETAILS_QUERY, { submissionId });
278
+ return data.submissionDetails;
279
+ }
225
280
  async testSolution(titleSlug, code, lang, testcases, questionId) {
226
281
  const response = await this.client.post(`problems/${titleSlug}/interpret_solution/`, {
227
282
  json: {
@@ -633,6 +688,33 @@ function formatStatus(status) {
633
688
  return chalk2.gray("-");
634
689
  }
635
690
  }
691
+ function displaySubmissionsList(submissions) {
692
+ const table = new Table({
693
+ head: [
694
+ chalk2.cyan("ID"),
695
+ chalk2.cyan("Status"),
696
+ chalk2.cyan("Lang"),
697
+ chalk2.cyan("Runtime"),
698
+ chalk2.cyan("Memory"),
699
+ chalk2.cyan("Date")
700
+ ],
701
+ colWidths: [12, 18, 15, 12, 12, 25],
702
+ style: { head: [], border: [] }
703
+ });
704
+ for (const s of submissions) {
705
+ const isAC = s.statusDisplay === "Accepted";
706
+ const cleanTime = new Date(parseInt(s.timestamp) * 1e3).toLocaleString();
707
+ table.push([
708
+ s.id,
709
+ isAC ? chalk2.green(s.statusDisplay) : chalk2.red(s.statusDisplay),
710
+ s.lang,
711
+ s.runtime,
712
+ s.memory,
713
+ cleanTime
714
+ ]);
715
+ }
716
+ console.log(table.toString());
717
+ }
636
718
 
637
719
  // src/commands/list.ts
638
720
  async function listCommand(options) {
@@ -1253,9 +1335,135 @@ async function dailyCommand(options) {
1253
1335
  }
1254
1336
  }
1255
1337
 
1338
+ // src/commands/random.ts
1339
+ import ora9 from "ora";
1340
+ import chalk10 from "chalk";
1341
+ async function randomCommand(options) {
1342
+ const credentials = config.getCredentials();
1343
+ if (credentials) {
1344
+ leetcodeClient.setCredentials(credentials);
1345
+ }
1346
+ const spinner = ora9("Fetching random problem...").start();
1347
+ try {
1348
+ const filters = {};
1349
+ if (options.difficulty) {
1350
+ const diffMap = {
1351
+ easy: "EASY",
1352
+ e: "EASY",
1353
+ medium: "MEDIUM",
1354
+ m: "MEDIUM",
1355
+ hard: "HARD",
1356
+ h: "HARD"
1357
+ };
1358
+ const diff = diffMap[options.difficulty.toLowerCase()];
1359
+ if (diff) {
1360
+ filters.difficulty = diff;
1361
+ } else {
1362
+ spinner.fail(`Invalid difficulty: ${options.difficulty}`);
1363
+ return;
1364
+ }
1365
+ }
1366
+ if (options.tag) {
1367
+ filters.tags = [options.tag];
1368
+ }
1369
+ const titleSlug = await leetcodeClient.getRandomProblem(filters);
1370
+ spinner.succeed("Found random problem!");
1371
+ console.log();
1372
+ if (options.pick) {
1373
+ await pickCommand(titleSlug, { open: options.open ?? true });
1374
+ } else {
1375
+ await showCommand(titleSlug);
1376
+ console.log(chalk10.gray("Run following to start solving:"));
1377
+ console.log(chalk10.cyan(` leetcode pick ${titleSlug}`));
1378
+ }
1379
+ } catch (error) {
1380
+ spinner.fail("Failed to fetch random problem");
1381
+ if (error instanceof Error) {
1382
+ console.log(chalk10.red(error.message));
1383
+ }
1384
+ }
1385
+ }
1386
+
1387
+ // src/commands/submissions.ts
1388
+ import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
1389
+ import { existsSync as existsSync4 } from "fs";
1390
+ import { join as join5 } from "path";
1391
+ import ora10 from "ora";
1392
+ import chalk11 from "chalk";
1393
+ async function submissionsCommand(idOrSlug, options) {
1394
+ const credentials = config.getCredentials();
1395
+ if (!credentials) {
1396
+ console.log(chalk11.red("Please login first to view submissions."));
1397
+ return;
1398
+ }
1399
+ leetcodeClient.setCredentials(credentials);
1400
+ const spinner = ora10("Fetching problem info...").start();
1401
+ try {
1402
+ let problem;
1403
+ if (/^\d+$/.test(idOrSlug)) {
1404
+ problem = await leetcodeClient.getProblemById(idOrSlug);
1405
+ } else {
1406
+ problem = await leetcodeClient.getProblem(idOrSlug);
1407
+ }
1408
+ if (!problem) {
1409
+ spinner.fail(`Problem "${idOrSlug}" not found`);
1410
+ return;
1411
+ }
1412
+ const slug = problem.titleSlug;
1413
+ spinner.text = "Fetching submissions...";
1414
+ const limit = options.limit ? parseInt(options.limit, 10) : 20;
1415
+ const submissions = await leetcodeClient.getSubmissionList(slug, limit);
1416
+ spinner.stop();
1417
+ if (submissions.length === 0) {
1418
+ console.log(chalk11.yellow("No submissions found."));
1419
+ return;
1420
+ }
1421
+ if (options.last) {
1422
+ const lastAC = submissions.find((s) => s.statusDisplay === "Accepted");
1423
+ if (lastAC) {
1424
+ console.log(chalk11.bold("Last Accepted Submission:"));
1425
+ displaySubmissionsList([lastAC]);
1426
+ } else {
1427
+ console.log(chalk11.yellow("No accepted submissions found in recent history."));
1428
+ }
1429
+ } else {
1430
+ displaySubmissionsList(submissions);
1431
+ }
1432
+ if (options.download) {
1433
+ const downloadSpinner = ora10("Downloading submission...").start();
1434
+ const lastAC = submissions.find((s) => s.statusDisplay === "Accepted");
1435
+ if (!lastAC) {
1436
+ downloadSpinner.fail("No accepted submission found to download.");
1437
+ return;
1438
+ }
1439
+ const details = await leetcodeClient.getSubmissionDetails(parseInt(lastAC.id, 10));
1440
+ const workDir = config.getWorkDir();
1441
+ const difficulty = problem.difficulty;
1442
+ const category = problem.topicTags.length > 0 ? problem.topicTags[0].name.replace(/[^\w\s-]/g, "").trim() : "Uncategorized";
1443
+ const targetDir = join5(workDir, difficulty, category);
1444
+ if (!existsSync4(targetDir)) {
1445
+ await mkdir2(targetDir, { recursive: true });
1446
+ }
1447
+ const langSlug = details.lang.name;
1448
+ const supportedLang = LANG_SLUG_MAP[langSlug] ?? "txt";
1449
+ const ext = LANGUAGE_EXTENSIONS[supportedLang] ?? langSlug;
1450
+ const fileName = `${problem.questionFrontendId}.${problem.titleSlug}.submission-${lastAC.id}.${ext}`;
1451
+ const filePath = join5(targetDir, fileName);
1452
+ await writeFile2(filePath, details.code, "utf-8");
1453
+ downloadSpinner.succeed(`Downloaded to ${chalk11.green(fileName)}`);
1454
+ console.log(chalk11.gray(`Path: ${filePath}`));
1455
+ }
1456
+ } catch (error) {
1457
+ spinner.fail("Failed to fetch submissions");
1458
+ if (error instanceof Error) {
1459
+ console.log(chalk11.red(error.message));
1460
+ }
1461
+ }
1462
+ }
1463
+
1256
1464
  // src/commands/config.ts
1257
1465
  import inquirer2 from "inquirer";
1258
- import chalk10 from "chalk";
1466
+ import chalk12 from "chalk";
1259
1467
  var SUPPORTED_LANGUAGES = [
1260
1468
  "typescript",
1261
1469
  "javascript",
@@ -1277,20 +1485,20 @@ async function configCommand(options) {
1277
1485
  if (options.lang) {
1278
1486
  const lang = options.lang.toLowerCase();
1279
1487
  if (!SUPPORTED_LANGUAGES.includes(lang)) {
1280
- console.log(chalk10.red(`Unsupported language: ${options.lang}`));
1281
- console.log(chalk10.gray(`Supported: ${SUPPORTED_LANGUAGES.join(", ")}`));
1488
+ console.log(chalk12.red(`Unsupported language: ${options.lang}`));
1489
+ console.log(chalk12.gray(`Supported: ${SUPPORTED_LANGUAGES.join(", ")}`));
1282
1490
  return;
1283
1491
  }
1284
1492
  config.setLanguage(lang);
1285
- console.log(chalk10.green(`\u2713 Default language set to ${lang}`));
1493
+ console.log(chalk12.green(`\u2713 Default language set to ${lang}`));
1286
1494
  }
1287
1495
  if (options.editor) {
1288
1496
  config.setEditor(options.editor);
1289
- console.log(chalk10.green(`\u2713 Editor set to ${options.editor}`));
1497
+ console.log(chalk12.green(`\u2713 Editor set to ${options.editor}`));
1290
1498
  }
1291
1499
  if (options.workdir) {
1292
1500
  config.setWorkDir(options.workdir);
1293
- console.log(chalk10.green(`\u2713 Work directory set to ${options.workdir}`));
1501
+ console.log(chalk12.green(`\u2713 Work directory set to ${options.workdir}`));
1294
1502
  }
1295
1503
  }
1296
1504
  async function configInteractiveCommand() {
@@ -1320,40 +1528,41 @@ async function configInteractiveCommand() {
1320
1528
  config.setEditor(answers.editor);
1321
1529
  config.setWorkDir(answers.workDir);
1322
1530
  console.log();
1323
- console.log(chalk10.green("\u2713 Configuration saved"));
1531
+ console.log(chalk12.green("\u2713 Configuration saved"));
1324
1532
  showCurrentConfig();
1325
1533
  }
1326
1534
  function showCurrentConfig() {
1327
1535
  const currentConfig = config.getConfig();
1328
1536
  const credentials = config.getCredentials();
1329
1537
  console.log();
1330
- console.log(chalk10.bold("LeetCode CLI Configuration"));
1331
- console.log(chalk10.gray("\u2500".repeat(40)));
1538
+ console.log(chalk12.bold("LeetCode CLI Configuration"));
1539
+ console.log(chalk12.gray("\u2500".repeat(40)));
1332
1540
  console.log();
1333
- console.log(chalk10.gray("Config file:"), config.getPath());
1541
+ console.log(chalk12.gray("Config file:"), config.getPath());
1334
1542
  console.log();
1335
- console.log(chalk10.gray("Language: "), chalk10.white(currentConfig.language));
1336
- console.log(chalk10.gray("Editor: "), chalk10.white(currentConfig.editor ?? "(not set)"));
1337
- console.log(chalk10.gray("Work Dir: "), chalk10.white(currentConfig.workDir));
1338
- console.log(chalk10.gray("Logged in: "), credentials ? chalk10.green("Yes") : chalk10.yellow("No"));
1543
+ console.log(chalk12.gray("Language: "), chalk12.white(currentConfig.language));
1544
+ console.log(chalk12.gray("Editor: "), chalk12.white(currentConfig.editor ?? "(not set)"));
1545
+ console.log(chalk12.gray("Work Dir: "), chalk12.white(currentConfig.workDir));
1546
+ console.log(chalk12.gray("Logged in: "), credentials ? chalk12.green("Yes") : chalk12.yellow("No"));
1339
1547
  }
1340
1548
 
1341
1549
  // src/index.ts
1342
1550
  var program = new Command();
1343
1551
  program.configureHelp({
1344
1552
  sortSubcommands: true,
1345
- subcommandTerm: (cmd) => chalk11.cyan(cmd.name()) + (cmd.alias() ? chalk11.gray(`|${cmd.alias()}`) : ""),
1346
- subcommandDescription: (cmd) => chalk11.white(cmd.description()),
1347
- optionTerm: (option) => chalk11.yellow(option.flags),
1348
- optionDescription: (option) => chalk11.white(option.description)
1553
+ subcommandTerm: (cmd) => chalk13.cyan(cmd.name()) + (cmd.alias() ? chalk13.gray(`|${cmd.alias()}`) : ""),
1554
+ subcommandDescription: (cmd) => chalk13.white(cmd.description()),
1555
+ optionTerm: (option) => chalk13.yellow(option.flags),
1556
+ optionDescription: (option) => chalk13.white(option.description)
1349
1557
  });
1350
- program.name("leetcode").usage("[command] [options]").description(chalk11.bold.cyan("\u{1F525} A modern LeetCode CLI built with TypeScript")).version("1.0.1", "-v, --version", "Output the version number").helpOption("-h, --help", "Display help for command").addHelpText("after", `
1351
- ${chalk11.yellow("Examples:")}
1352
- ${chalk11.cyan("$ leetcode login")} Login to LeetCode
1353
- ${chalk11.cyan("$ leetcode list -d easy")} List easy problems
1354
- ${chalk11.cyan("$ leetcode pick 1")} Start solving "Two Sum"
1355
- ${chalk11.cyan("$ leetcode test 1")} Test your solution
1356
- ${chalk11.cyan("$ leetcode submit 1")} Submit your solution
1558
+ program.name("leetcode").usage("[command] [options]").description(chalk13.bold.cyan("\u{1F525} A modern LeetCode CLI built with TypeScript")).version("1.1.0", "-v, --version", "Output the version number").helpOption("-h, --help", "Display help for command").addHelpText("after", `
1559
+ ${chalk13.yellow("Examples:")}
1560
+ ${chalk13.cyan("$ leetcode login")} Login to LeetCode
1561
+ ${chalk13.cyan("$ leetcode list -d easy")} List easy problems
1562
+ ${chalk13.cyan("$ leetcode random -d medium")} Get random medium problem
1563
+ ${chalk13.cyan("$ leetcode pick 1")} Start solving "Two Sum"
1564
+ ${chalk13.cyan("$ leetcode test 1")} Test your solution
1565
+ ${chalk13.cyan("$ leetcode submit 1")} Submit your solution
1357
1566
  `);
1358
1567
  program.command("login").description("Login to LeetCode with browser cookies").action(loginCommand);
1359
1568
  program.command("logout").description("Clear stored credentials").action(logoutCommand);
@@ -1361,9 +1570,11 @@ program.command("whoami").description("Check current login status").action(whoam
1361
1570
  program.command("list").alias("l").description("List LeetCode problems").option("-d, --difficulty <level>", "Filter by difficulty (easy/medium/hard)").option("-s, --status <status>", "Filter by status (todo/solved/attempted)").option("-t, --tag <tags...>", "Filter by topic tags").option("-q, --search <keywords>", "Search by keywords").option("-n, --limit <number>", "Number of problems to show", "20").option("-p, --page <number>", "Page number", "1").action(listCommand);
1362
1571
  program.command("show <id>").alias("s").description("Show problem description").action(showCommand);
1363
1572
  program.command("daily").alias("d").description("Show today's daily challenge").action(dailyCommand);
1573
+ program.command("random").alias("r").description("Get a random problem").option("-d, --difficulty <level>", "Filter by difficulty (easy/medium/hard)").option("-t, --tag <tag>", "Filter by topic tag").option("--pick", "Auto-generate solution file").option("--no-open", "Do not open file in editor").action(randomCommand);
1364
1574
  program.command("pick <id>").alias("p").description("Generate solution file for a problem").option("-l, --lang <language>", "Programming language for the solution").option("--no-open", "Do not open file in editor").action(pickCommand);
1365
1575
  program.command("test <file>").alias("t").description("Test solution against sample test cases").option("-c, --testcase <testcase>", "Custom test case").action(testCommand);
1366
1576
  program.command("submit <file>").alias("x").description("Submit solution to LeetCode").action(submitCommand);
1577
+ program.command("submissions <id>").description("View past submissions").option("-n, --limit <number>", "Number of submissions to show", "20").option("--last", "Show details of the last accepted submission").option("--download", "Download the last accepted submission code").action(submissionsCommand);
1367
1578
  program.command("stat [username]").description("Show user statistics").action(statCommand);
1368
1579
  program.command("config").description("View or set configuration").option("-l, --lang <language>", "Set default programming language").option("-e, --editor <editor>", "Set editor command").option("-w, --workdir <path>", "Set working directory for solutions").option("-i, --interactive", "Interactive configuration").action(async (options) => {
1369
1580
  if (options.interactive) {
@@ -1376,8 +1587,8 @@ program.showHelpAfterError("(add --help for additional information)");
1376
1587
  program.parse();
1377
1588
  if (!process.argv.slice(2).length) {
1378
1589
  console.log();
1379
- console.log(chalk11.bold.cyan(" \u{1F525} LeetCode CLI"));
1380
- console.log(chalk11.gray(" A modern command-line interface for LeetCode"));
1590
+ console.log(chalk13.bold.cyan(" \u{1F525} LeetCode CLI"));
1591
+ console.log(chalk13.gray(" A modern command-line interface for LeetCode"));
1381
1592
  console.log();
1382
1593
  program.outputHelp();
1383
1594
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@night-slayer18/leetcode-cli",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "A modern LeetCode CLI built with TypeScript",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",