@zxr55555/leetmate 0.2.8

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 LeetMate contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO CLAIM SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,11 @@
1
+ # @zxr55555/leetmate
2
+
3
+ Install the LeetMate terminal CLI through npm.
4
+
5
+ ```bash
6
+ npm install -g @zxr55555/leetmate
7
+ ```
8
+
9
+ This package downloads the matching prebuilt binary from the [LeetMate GitHub Releases](https://github.com/DuckInAShirt/leetmate/releases), verifies it against `checksums.txt`, and exposes it as `leetmate`.
10
+
11
+ LeetMate also requires the external [`leetgo`](https://github.com/j178/leetgo) CLI in your `PATH` and a configured leetgo workspace.
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const { spawnSync } = require('child_process');
7
+
8
+ const exe = process.platform === 'win32' ? 'leetmate.exe' : 'leetmate';
9
+ const binary = path.join(__dirname, exe);
10
+
11
+ if (!fs.existsSync(binary)) {
12
+ console.error('leetmate binary is missing. Reinstall the package or run `npm rebuild leetmate`.');
13
+ process.exit(1);
14
+ }
15
+
16
+ const result = spawnSync(binary, process.argv.slice(2), { stdio: 'inherit' });
17
+ if (result.error) {
18
+ console.error(result.error.message);
19
+ process.exit(1);
20
+ }
21
+ process.exit(result.status === null ? 1 : result.status);
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@zxr55555/leetmate",
3
+ "version": "0.2.8",
4
+ "description": "Hints, not handouts: a terminal LeetCode coach and spaced review tool.",
5
+ "license": "MIT",
6
+ "homepage": "https://github.com/DuckInAShirt/leetmate#readme",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/DuckInAShirt/leetmate.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/DuckInAShirt/leetmate/issues"
13
+ },
14
+ "bin": {
15
+ "leetmate": "bin/leetmate.js"
16
+ },
17
+ "scripts": {
18
+ "postinstall": "node scripts/install.js",
19
+ "test": "node --test scripts/*.test.js",
20
+ "test:install": "LEETMATE_SKIP_DOWNLOAD=1 node scripts/install.js && node bin/leetmate.js --version"
21
+ },
22
+ "files": [
23
+ "bin/",
24
+ "scripts/",
25
+ "README.md",
26
+ "LICENSE"
27
+ ],
28
+ "keywords": [
29
+ "leetcode",
30
+ "tui",
31
+ "cli",
32
+ "algorithms",
33
+ "spaced-repetition"
34
+ ],
35
+ "engines": {
36
+ "node": ">=18"
37
+ },
38
+ "publishConfig": {
39
+ "access": "public"
40
+ }
41
+ }
@@ -0,0 +1,155 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const crypto = require('crypto');
5
+ const fs = require('fs');
6
+ const https = require('https');
7
+ const os = require('os');
8
+ const path = require('path');
9
+ const { execFileSync } = require('child_process');
10
+
11
+ const root = path.resolve(__dirname, '..');
12
+ const binDir = path.join(root, 'bin');
13
+ const packageJSON = require(path.join(root, 'package.json'));
14
+ const owner = 'DuckInAShirt';
15
+ const repo = 'leetmate';
16
+
17
+ function platformTarget(platform = os.platform(), arch = os.arch()) {
18
+ const osName = platform === 'darwin' ? 'macOS' : platform === 'win32' ? 'Windows' : platform === 'linux' ? 'linux' : '';
19
+ const archName = arch === 'x64' ? 'amd64' : arch === 'arm64' ? 'arm64' : '';
20
+ if (!osName || !archName) {
21
+ throw new Error(`Unsupported platform: ${platform}/${arch}`);
22
+ }
23
+ return {
24
+ osName,
25
+ archName,
26
+ ext: platform === 'win32' ? '.zip' : '.tar.gz',
27
+ exe: platform === 'win32' ? 'leetmate.exe' : 'leetmate',
28
+ };
29
+ }
30
+
31
+ function releaseVersion(env = process.env, pkg = packageJSON) {
32
+ if (env.LEETMATE_VERSION) {
33
+ return env.LEETMATE_VERSION.replace(/^v/, '');
34
+ }
35
+ return pkg.version.replace(/^v/, '');
36
+ }
37
+
38
+ function assetName(version, target) {
39
+ return `leetmate_${version}_${target.osName}_${target.archName}${target.ext}`;
40
+ }
41
+
42
+ function releaseURL(version, name) {
43
+ return `https://github.com/${owner}/${repo}/releases/download/v${version}/${name}`;
44
+ }
45
+
46
+ function binaryPath(target) {
47
+ return path.join(binDir, target.exe);
48
+ }
49
+
50
+ function parseChecksums(text) {
51
+ const out = new Map();
52
+ for (const line of text.split(/\r?\n/)) {
53
+ const trimmed = line.trim();
54
+ if (!trimmed) continue;
55
+ const match = trimmed.match(/^([a-fA-F0-9]{64})\s+(.+)$/);
56
+ if (match) {
57
+ out.set(match[2].replace(/^\*?/, ''), match[1].toLowerCase());
58
+ }
59
+ }
60
+ return out;
61
+ }
62
+
63
+ function sha256(file) {
64
+ const hash = crypto.createHash('sha256');
65
+ hash.update(fs.readFileSync(file));
66
+ return hash.digest('hex');
67
+ }
68
+
69
+ function download(url, dest) {
70
+ return new Promise((resolve, reject) => {
71
+ const file = fs.createWriteStream(dest);
72
+ const request = https.get(url, { headers: { 'User-Agent': 'leetmate-npm-installer' } }, (response) => {
73
+ if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
74
+ file.close(() => fs.rmSync(dest, { force: true }));
75
+ download(response.headers.location, dest).then(resolve, reject);
76
+ return;
77
+ }
78
+ if (response.statusCode !== 200) {
79
+ file.close(() => fs.rmSync(dest, { force: true }));
80
+ reject(new Error(`Download failed: HTTP ${response.statusCode} ${url}`));
81
+ return;
82
+ }
83
+ response.pipe(file);
84
+ file.on('finish', () => file.close(resolve));
85
+ });
86
+ request.on('error', (err) => {
87
+ file.close(() => fs.rmSync(dest, { force: true }));
88
+ reject(err);
89
+ });
90
+ });
91
+ }
92
+
93
+ function extract(archive, target) {
94
+ fs.mkdirSync(binDir, { recursive: true });
95
+ const binary = binaryPath(target);
96
+ fs.rmSync(binary, { force: true });
97
+ if (archive.endsWith('.zip')) {
98
+ const tmpDir = `${archive}-extract`;
99
+ fs.rmSync(tmpDir, { recursive: true, force: true });
100
+ fs.mkdirSync(tmpDir, { recursive: true });
101
+ execFileSync('powershell', ['-NoProfile', '-Command', `Expand-Archive -LiteralPath ${JSON.stringify(archive)} -DestinationPath ${JSON.stringify(tmpDir)} -Force`], { stdio: 'inherit' });
102
+ fs.copyFileSync(path.join(tmpDir, target.exe), binary);
103
+ fs.rmSync(tmpDir, { recursive: true, force: true });
104
+ } else {
105
+ execFileSync('tar', ['-xzf', archive, '-C', binDir, target.exe], { stdio: 'inherit' });
106
+ }
107
+ if (process.platform !== 'win32') {
108
+ fs.chmodSync(binary, 0o755);
109
+ }
110
+ }
111
+
112
+ async function install() {
113
+ const target = platformTarget();
114
+ const skipDownload = process.argv.includes('--skip-download') || process.env.LEETMATE_SKIP_DOWNLOAD === '1';
115
+ if (skipDownload) {
116
+ fs.mkdirSync(binDir, { recursive: true });
117
+ const binary = binaryPath(target);
118
+ if (!fs.existsSync(binary)) {
119
+ fs.writeFileSync(binary, '#!/bin/sh\necho leetmate development\n', { mode: 0o755 });
120
+ }
121
+ return;
122
+ }
123
+
124
+ const version = releaseVersion();
125
+ if (!version || version === '0.0.0-dev') {
126
+ throw new Error('Cannot install development npm package without LEETMATE_VERSION. Use a published release package.');
127
+ }
128
+
129
+ const name = assetName(version, target);
130
+ const tmp = path.join(os.tmpdir(), `${name}-${process.pid}`);
131
+ const sums = path.join(os.tmpdir(), `leetmate-checksums-${version}-${process.pid}.txt`);
132
+ await download(releaseURL(version, name), tmp);
133
+ await download(releaseURL(version, 'checksums.txt'), sums);
134
+ const checksums = parseChecksums(fs.readFileSync(sums, 'utf8'));
135
+ const expected = checksums.get(name);
136
+ if (!expected) {
137
+ throw new Error(`No checksum found for ${name}`);
138
+ }
139
+ const actual = sha256(tmp);
140
+ if (actual !== expected) {
141
+ throw new Error(`Checksum mismatch for ${name}: expected ${expected}, got ${actual}`);
142
+ }
143
+ extract(tmp, target);
144
+ fs.rmSync(tmp, { force: true });
145
+ fs.rmSync(sums, { force: true });
146
+ }
147
+
148
+ if (require.main === module) {
149
+ install().catch((err) => {
150
+ console.error(`leetmate install failed: ${err.message}`);
151
+ process.exit(1);
152
+ });
153
+ }
154
+
155
+ module.exports = { assetName, binaryPath, parseChecksums, platformTarget, releaseURL, releaseVersion };
@@ -0,0 +1,47 @@
1
+ 'use strict';
2
+
3
+ const assert = require('node:assert/strict');
4
+ const test = require('node:test');
5
+ const path = require('node:path');
6
+ const {
7
+ assetName,
8
+ binaryPath,
9
+ parseChecksums,
10
+ platformTarget,
11
+ releaseURL,
12
+ releaseVersion,
13
+ } = require('./install');
14
+
15
+ test('platformTarget maps supported platforms to GoReleaser names', () => {
16
+ assert.deepEqual(platformTarget('darwin', 'arm64'), { osName: 'macOS', archName: 'arm64', ext: '.tar.gz', exe: 'leetmate' });
17
+ assert.deepEqual(platformTarget('linux', 'x64'), { osName: 'linux', archName: 'amd64', ext: '.tar.gz', exe: 'leetmate' });
18
+ assert.deepEqual(platformTarget('win32', 'x64'), { osName: 'Windows', archName: 'amd64', ext: '.zip', exe: 'leetmate.exe' });
19
+ });
20
+
21
+ test('platformTarget rejects unsupported combinations', () => {
22
+ assert.throws(() => platformTarget('freebsd', 'x64'), /Unsupported platform/);
23
+ assert.throws(() => platformTarget('linux', 'arm'), /Unsupported platform/);
24
+ });
25
+
26
+ test('assetName and releaseURL match GoReleaser assets', () => {
27
+ const target = platformTarget('darwin', 'arm64');
28
+ const name = assetName('0.2.7', target);
29
+ assert.equal(name, 'leetmate_0.2.7_macOS_arm64.tar.gz');
30
+ assert.equal(releaseURL('0.2.7', name), 'https://github.com/DuckInAShirt/leetmate/releases/download/v0.2.7/leetmate_0.2.7_macOS_arm64.tar.gz');
31
+ });
32
+
33
+ test('parseChecksums extracts sha256 entries', () => {
34
+ const sum = 'a'.repeat(64);
35
+ const parsed = parseChecksums(`${sum} leetmate_0.2.7_linux_amd64.tar.gz\n`);
36
+ assert.equal(parsed.get('leetmate_0.2.7_linux_amd64.tar.gz'), sum);
37
+ });
38
+
39
+ test('releaseVersion prefers LEETMATE_VERSION', () => {
40
+ assert.equal(releaseVersion({ LEETMATE_VERSION: 'v1.2.3' }, { version: '0.0.0-dev' }), '1.2.3');
41
+ assert.equal(releaseVersion({}, { version: '1.2.4' }), '1.2.4');
42
+ });
43
+
44
+ test('binaryPath points inside npm bin directory', () => {
45
+ assert.equal(path.basename(binaryPath(platformTarget('win32', 'x64'))), 'leetmate.exe');
46
+ assert.equal(path.basename(binaryPath(platformTarget('linux', 'x64'))), 'leetmate');
47
+ });