@deloskiyt/samcli 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 +107 -0
- package/package.json +39 -0
- package/samcli.js +258 -0
package/README.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# samcli
|
|
2
|
+
|
|
3
|
+
An interactive, zero-dependency, ultra-lightweight Node.js command-line utility to query the Samsung CheckFirm API.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Interactive Mode**: Runs a step-by-step wizard to prompt you for the Model, CSC/Region, and API Key (with smart defaults).
|
|
8
|
+
- **Direct CLI Mode**: Pass parameters directly (`node samcli.js <model> <csc> [api_key]`) for instant results.
|
|
9
|
+
- **Zero Dependencies**: Pure Node.js! No need for `npm install` or massive `node_modules` folders.
|
|
10
|
+
- **Rich UI/UX**: Includes an ASCII art header, animated loader spinner, and colored box-drawing layout output.
|
|
11
|
+
- **Rate-Limited by default**: Free tier allows up to 2 queries per day per IP. You can bypass the limit by providing a valid API key.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Special Acknowledgments
|
|
16
|
+
|
|
17
|
+
### 🌟 Special Thanks to **killshot**!
|
|
18
|
+
This tool and its UI improvements were created with key contributions and inspiration from **killshot**. Thank you for helping shape the user experience!
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
Ensure you have [Node.js](https://nodejs.org/) installed (v12.0.0 or higher).
|
|
25
|
+
|
|
26
|
+
### Method 1: Global CLI Install (Recommended)
|
|
27
|
+
|
|
28
|
+
You can install this tool globally directly from the NPM Registry:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# Using npm
|
|
32
|
+
npm install -g @deloskiyt/samcli
|
|
33
|
+
|
|
34
|
+
# Using yarn
|
|
35
|
+
yarn global add @deloskiyt/samcli
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Or install it directly from GitHub:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# Using npm
|
|
42
|
+
npm install -g ktopytal/samcli
|
|
43
|
+
|
|
44
|
+
# Using yarn
|
|
45
|
+
yarn global add ktopytal/samcli
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Once installed, you can run the tool from anywhere in your terminal using the `samcli` command!
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
### Method 2: Local Run (No Install)
|
|
52
|
+
|
|
53
|
+
1. Clone this repository:
|
|
54
|
+
```bash
|
|
55
|
+
git clone https://github.com/ktopytal/samcli.git
|
|
56
|
+
cd samcli
|
|
57
|
+
```
|
|
58
|
+
2. Run the script directly:
|
|
59
|
+
```bash
|
|
60
|
+
node samcli.js
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Usage Examples
|
|
66
|
+
|
|
67
|
+
### 1. Interactive Mode (Wizard)
|
|
68
|
+
Simply execute the command without arguments:
|
|
69
|
+
```bash
|
|
70
|
+
# If installed globally
|
|
71
|
+
samcli
|
|
72
|
+
|
|
73
|
+
# If running locally
|
|
74
|
+
node samcli.js
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Follow the prompts in your terminal:
|
|
78
|
+
- Enter Model (e.g. `SM-S928B`)
|
|
79
|
+
- Enter Region/CSC (e.g. `EUX`)
|
|
80
|
+
- Enter API Key (optional, press Enter to use the free tier limit of 2 requests per day)
|
|
81
|
+
|
|
82
|
+
### 2. Direct CLI Arguments
|
|
83
|
+
Check a device instantly:
|
|
84
|
+
```bash
|
|
85
|
+
# If installed globally
|
|
86
|
+
samcli SM-S928B EUX
|
|
87
|
+
|
|
88
|
+
# If running locally
|
|
89
|
+
node samcli.js SM-S928B EUX
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Query with a private API key to bypass limits:
|
|
93
|
+
```bash
|
|
94
|
+
# If installed globally
|
|
95
|
+
samcli SM-S928B EUX your_api_key_here
|
|
96
|
+
|
|
97
|
+
# If running locally
|
|
98
|
+
node samcli.js SM-S928B EUX your_api_key_here
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## License
|
|
105
|
+
|
|
106
|
+
This project is licensed under the MIT License.
|
|
107
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@deloskiyt/samcli",
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
"version": "1.1.0",
|
|
6
|
+
"description": "Interactive zero-dependency CLI client for Samsung CheckFirm API",
|
|
7
|
+
"main": "samcli.js",
|
|
8
|
+
"preferGlobal": true,
|
|
9
|
+
"bin": {
|
|
10
|
+
"samcli": "./samcli.js"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"start": "node samcli.js"
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
"keywords": [
|
|
17
|
+
"samsung",
|
|
18
|
+
"checkfirm",
|
|
19
|
+
"firmware",
|
|
20
|
+
"cli",
|
|
21
|
+
"node",
|
|
22
|
+
"interactive",
|
|
23
|
+
"samcli"
|
|
24
|
+
],
|
|
25
|
+
"author": "shaza",
|
|
26
|
+
"contributors": [
|
|
27
|
+
"killshot"
|
|
28
|
+
],
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "git+https://github.com/ktopytal/samcli.git"
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=12.0.0"
|
|
38
|
+
}
|
|
39
|
+
}
|
package/samcli.js
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const readline = require('readline');
|
|
3
|
+
const https = require('https');
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
const rl = readline.createInterface({
|
|
7
|
+
input: process.stdin,
|
|
8
|
+
output: process.stdout
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const askQuestion = (query) => new Promise((resolve) => rl.question(query, resolve));
|
|
12
|
+
|
|
13
|
+
function makeRequest(url) {
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
https.get(url, (res) => {
|
|
16
|
+
let data = '';
|
|
17
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
18
|
+
res.on('end', () => {
|
|
19
|
+
try {
|
|
20
|
+
const parsed = JSON.parse(data);
|
|
21
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
22
|
+
resolve(parsed);
|
|
23
|
+
} else {
|
|
24
|
+
reject(new Error(parsed.error || parsed.detail || `Server error (${res.statusCode})`));
|
|
25
|
+
}
|
|
26
|
+
} catch (e) {
|
|
27
|
+
if (res.statusCode === 429) {
|
|
28
|
+
reject(new Error('Rate limit exceeded. Free limit is 2 queries per day per IP.'));
|
|
29
|
+
} else {
|
|
30
|
+
reject(new Error(`HTTP Error ${res.statusCode} (Failed to parse response)`));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}).on('error', (err) => {
|
|
35
|
+
reject(err);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function drawBox(title, rows) {
|
|
41
|
+
const width = 85;
|
|
42
|
+
const horizontalLine = '─'.repeat(width - 2);
|
|
43
|
+
|
|
44
|
+
console.log(`\x1b[36m┌${horizontalLine}┐\x1b[0m`);
|
|
45
|
+
// Center title
|
|
46
|
+
const paddingLeft = Math.floor((width - 2 - title.length) / 2);
|
|
47
|
+
const paddingRight = width - 2 - title.length - paddingLeft;
|
|
48
|
+
console.log(`\x1b[36m│\x1b[0m${' '.repeat(paddingLeft)}\x1b[1m\x1b[33m${title}\x1b[0m${' '.repeat(paddingRight)}\x1b[36m│\x1b[0m`);
|
|
49
|
+
console.log(`\x1b[36m├${horizontalLine}┤\x1b[0m`);
|
|
50
|
+
|
|
51
|
+
rows.forEach(row => {
|
|
52
|
+
if (row === 'sep') {
|
|
53
|
+
console.log(`\x1b[36m├${horizontalLine}┤\x1b[0m`);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const label = row.label;
|
|
57
|
+
const val = String(row.value || '');
|
|
58
|
+
const color = row.color || '\x1b[37m'; // Default white
|
|
59
|
+
|
|
60
|
+
// Format line: "│ Label: [Value] │"
|
|
61
|
+
const content = ` \x1b[1m${label.padEnd(26)}\x1b[0m ${color}${val}\x1b[0m`;
|
|
62
|
+
// Calculate length without ANSI escape codes for padding
|
|
63
|
+
const cleanContent = ` ${label.padEnd(26)} ${val}`;
|
|
64
|
+
const trailingSpaces = (width - 2) - cleanContent.length;
|
|
65
|
+
|
|
66
|
+
if (trailingSpaces >= 0) {
|
|
67
|
+
console.log(`\x1b[36m│\x1b[0m${content}${' '.repeat(trailingSpaces)}\x1b[36m│\x1b[0m`);
|
|
68
|
+
} else {
|
|
69
|
+
// Truncate safely if too long
|
|
70
|
+
const maxValLen = (width - 2) - 30; // 30 is label + padding
|
|
71
|
+
const truncatedVal = val.substring(0, maxValLen - 3) + '...';
|
|
72
|
+
const truncContent = ` \x1b[1m${label.padEnd(26)}\x1b[0m ${color}${truncatedVal}\x1b[0m`;
|
|
73
|
+
const cleanTruncContent = ` ${label.padEnd(26)} ${truncatedVal}`;
|
|
74
|
+
const truncTrailing = (width - 2) - cleanTruncContent.length;
|
|
75
|
+
console.log(`\x1b[36m│\x1b[0m${truncContent}${' '.repeat(truncTrailing)}\x1b[36m│\x1b[0m`);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
console.log(`\x1b[36m└${horizontalLine}┘\x1b[0m`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const CURRENT_VERSION = '1.1.0';
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
function fetchRemoteVersion() {
|
|
86
|
+
return new Promise((resolve) => {
|
|
87
|
+
const req = https.get('https://raw.githubusercontent.com/ktopytal/samcli/main/package.json', { timeout: 3000 }, (res) => {
|
|
88
|
+
let data = '';
|
|
89
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
90
|
+
res.on('end', () => {
|
|
91
|
+
try {
|
|
92
|
+
const pkg = JSON.parse(data);
|
|
93
|
+
resolve(pkg.version || null);
|
|
94
|
+
} catch (e) {
|
|
95
|
+
resolve(null);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
req.on('error', () => resolve(null));
|
|
100
|
+
req.on('timeout', () => { req.destroy(); resolve(null); });
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function isNewerVersion(local, remote) {
|
|
105
|
+
const localParts = local.split('.').map(Number);
|
|
106
|
+
const remoteParts = remote.split('.').map(Number);
|
|
107
|
+
for (let i = 0; i < 3; i++) {
|
|
108
|
+
if (remoteParts[i] > localParts[i]) return true;
|
|
109
|
+
if (remoteParts[i] < localParts[i]) return false;
|
|
110
|
+
}
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function runUpdate() {
|
|
115
|
+
const { execSync } = require('child_process');
|
|
116
|
+
console.log('\n \x1b[33mUpdating SamCLI to the latest version...\x1b[0m');
|
|
117
|
+
try {
|
|
118
|
+
execSync('npm install -g ktopytal/samcli', { stdio: 'inherit' });
|
|
119
|
+
console.log(' \x1b[32m✔ Successfully updated! Please restart the tool.\x1b[0m\n');
|
|
120
|
+
process.exit(0);
|
|
121
|
+
} catch (e) {
|
|
122
|
+
console.log(' \x1b[31m✘ Update failed. Please run manually: npm install -g ktopytal/samcli\x1b[0m\n');
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function printHeader() {
|
|
127
|
+
console.clear();
|
|
128
|
+
console.log(`\x1b[36m
|
|
129
|
+
____ _ _ _____ _cm
|
|
130
|
+
/ ___| |__ ___ ___| | __| ___(_)_ __ _ __ ___
|
|
131
|
+
| | | '_ \\ / _ \\/ __| |/ /| |_ | | '__| '_ \` _ \\
|
|
132
|
+
| |___| | | | __/ (__| < | _| | | | | | | | | |
|
|
133
|
+
\\____|_| |_|\\___|\\___|_|\\_\\|_| |_|_| |_| |_| |_|
|
|
134
|
+
\x1b[0m`);
|
|
135
|
+
console.log('\x1b[35m =======================================================\x1b[0m');
|
|
136
|
+
console.log('\x1b[1m SamCLI v1.1.0 • Public Edition\x1b[0m');
|
|
137
|
+
|
|
138
|
+
console.log('\x1b[90m Special thanks to \x1b[33mkillshot\x1b[90m for inspiration & contributions!\x1b[0m');
|
|
139
|
+
console.log('\x1b[35m =======================================================\x1b[0m\n');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function main() {
|
|
143
|
+
const remoteVersionPromise = fetchRemoteVersion();
|
|
144
|
+
const args = process.argv.slice(2);
|
|
145
|
+
|
|
146
|
+
// Support version check commands
|
|
147
|
+
if (args.length > 0) {
|
|
148
|
+
const firstArg = args[0].toLowerCase();
|
|
149
|
+
if (firstArg === '/version' || firstArg === '-v' || firstArg === '--version' || firstArg === 'version') {
|
|
150
|
+
rl.close();
|
|
151
|
+
console.log(`\x1b[36mSamCLI v${CURRENT_VERSION}\x1b[0m`);
|
|
152
|
+
console.log('\x1b[90mSpecial thanks to \x1b[33mkillshot\x1b[90m for inspiration & contributions!\x1b[0m');
|
|
153
|
+
process.exit(0);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
let model = '';
|
|
158
|
+
let csc = '';
|
|
159
|
+
let key = '';
|
|
160
|
+
|
|
161
|
+
if (args.length > 0) {
|
|
162
|
+
// CLI Arguments Mode
|
|
163
|
+
model = args[0] || 'SM-S928B';
|
|
164
|
+
csc = args[1] || 'EUX';
|
|
165
|
+
key = args[2] || '';
|
|
166
|
+
|
|
167
|
+
// Intelligent parameter shift if key is passed as second param:
|
|
168
|
+
if (csc.length > 5) {
|
|
169
|
+
key = csc;
|
|
170
|
+
csc = 'EUX';
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Remove "=" if copied by mistake
|
|
174
|
+
if (csc.startsWith('=')) csc = csc.substring(1);
|
|
175
|
+
if (key.startsWith('=')) key = key.substring(1);
|
|
176
|
+
|
|
177
|
+
rl.close();
|
|
178
|
+
printHeader();
|
|
179
|
+
} else {
|
|
180
|
+
// Interactive Mode
|
|
181
|
+
printHeader();
|
|
182
|
+
console.log('\x1b[90m (Press Enter to accept default values)\x1b[0m\n');
|
|
183
|
+
|
|
184
|
+
model = await askQuestion(' \x1b[32m? Enter Samsung Model (default: SM-S928B): \x1b[0m');
|
|
185
|
+
model = model.trim() || 'SM-S928B';
|
|
186
|
+
|
|
187
|
+
csc = await askQuestion(' \x1b[32m? Enter Region/CSC (default: EUX): \x1b[0m');
|
|
188
|
+
csc = csc.trim() || 'EUX';
|
|
189
|
+
|
|
190
|
+
key = await askQuestion(' \x1b[32m? Enter API Key (optional, press Enter for free limit): \x1b[0m');
|
|
191
|
+
key = key.trim();
|
|
192
|
+
|
|
193
|
+
rl.close();
|
|
194
|
+
printHeader();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Auto-update check
|
|
198
|
+
const remoteVersion = await remoteVersionPromise;
|
|
199
|
+
if (remoteVersion && isNewerVersion(CURRENT_VERSION, remoteVersion)) {
|
|
200
|
+
if (args.length > 0) {
|
|
201
|
+
console.log(` \x1b[33m📢 A new version of SamCLI is available: v${remoteVersion} (Current: v${CURRENT_VERSION})\x1b[0m`);
|
|
202
|
+
console.log(` \x1b[33m Run 'npm install -g ktopytal/samcli' to update.\x1b[0m\n`);
|
|
203
|
+
} else {
|
|
204
|
+
console.log(` \x1b[33m📢 A new version of SamCLI is available: v${remoteVersion} (Current: v${CURRENT_VERSION})\x1b[0m`);
|
|
205
|
+
const answer = await askQuestion(' \x1b[32m? Would you like to auto-update now? (Y/n): \x1b[0m');
|
|
206
|
+
if (answer.trim().toLowerCase() !== 'n') {
|
|
207
|
+
runUpdate();
|
|
208
|
+
}
|
|
209
|
+
printHeader();
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
console.log(' \x1b[90mPreparing API query...\x1b[0m');
|
|
214
|
+
|
|
215
|
+
// Build URL
|
|
216
|
+
const url = `https://api.checkfirm.pl/api/check/v2/${model}/${csc}?key=${key}`;
|
|
217
|
+
|
|
218
|
+
// Start Spinner
|
|
219
|
+
const spinnerFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
220
|
+
let spinnerIndex = 0;
|
|
221
|
+
const spinnerInterval = setInterval(() => {
|
|
222
|
+
process.stdout.write(`\r \x1b[33m${spinnerFrames[spinnerIndex++ % spinnerFrames.length]} Loading... Querying server, please wait...\x1b[0m`);
|
|
223
|
+
}, 80);
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
const data = await makeRequest(url);
|
|
227
|
+
clearInterval(spinnerInterval);
|
|
228
|
+
process.stdout.write('\r\x1b[K'); // Clear line
|
|
229
|
+
|
|
230
|
+
console.log(' \x1b[32m✔ Server responded successfully!\x1b[0m\n');
|
|
231
|
+
|
|
232
|
+
drawBox('SAMSUNG FIRMWARE DATA', [
|
|
233
|
+
{ label: 'Model Name:', value: `${data.model || model} (${data.region || csc})`, color: '\x1b[37m' },
|
|
234
|
+
'sep',
|
|
235
|
+
{ label: 'Official Version:', value: data.latest_official || 'N/A', color: '\x1b[32m' },
|
|
236
|
+
{ label: 'Internal Test Version:', value: data.latest_internal || 'N/A', color: '\x1b[36m' },
|
|
237
|
+
'sep',
|
|
238
|
+
{ label: 'Android / One UI Version:', value: `Android ${data.android_version || 'N/A'} / One UI ${data.oneui_version || 'N/A'}`, color: '\x1b[35m' },
|
|
239
|
+
{ label: 'Security Patch Level:', value: data.patch_level || 'N/A', color: '\x1b[33m' }
|
|
240
|
+
]);
|
|
241
|
+
console.log(`\n \x1b[90mQuery URL: ${url}\x1b[0m\n`);
|
|
242
|
+
} catch (err) {
|
|
243
|
+
clearInterval(spinnerInterval);
|
|
244
|
+
process.stdout.write('\r\x1b[K'); // Clear line
|
|
245
|
+
|
|
246
|
+
console.log(' \x1b[31m✘ Query failed!\x1b[0m\n');
|
|
247
|
+
|
|
248
|
+
drawBox('ERROR INFORMATION', [
|
|
249
|
+
{ label: 'Target Model:', value: `${model} (${csc})`, color: '\x1b[37m' },
|
|
250
|
+
{ label: 'Status:', value: 'Failed to retrieve data', color: '\x1b[31m' },
|
|
251
|
+
'sep',
|
|
252
|
+
{ label: 'Error Details:', value: err.message, color: '\x1b[31m' }
|
|
253
|
+
]);
|
|
254
|
+
console.log(`\n \x1b[90mQuery URL: ${url}\x1b[0m\n`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
main();
|