@backendsystems/nibble 0.3.1 → 0.4.1

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.
Files changed (2) hide show
  1. package/bin/install.js +79 -170
  2. package/package.json +4 -1
package/bin/install.js CHANGED
@@ -1,211 +1,120 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ const https = require('https');
3
4
  const fs = require('fs');
4
5
  const os = require('os');
5
6
  const path = require('path');
6
- const https = require('https');
7
7
  const crypto = require('crypto');
8
- const { spawnSync } = require('child_process');
8
+ const tar = require('tar');
9
9
 
10
10
  const PROJECT = 'nibble';
11
11
  const OWNER = 'backendsystems';
12
- const REPO = 'nibble';
13
12
  const ROOT = path.resolve(__dirname, '..');
14
13
  const VENDOR_DIR = path.join(ROOT, 'vendor');
15
- const PACKAGE_JSON = require(path.join(ROOT, 'package.json'));
16
-
17
- function mapPlatform() {
18
- const platformMap = {
19
- linux: 'linux',
20
- darwin: 'darwin',
21
- win32: 'windows',
22
- };
23
-
24
- const archMap = {
25
- x64: 'amd64',
26
- arm64: 'arm64',
27
- };
28
-
29
- const osName = platformMap[process.platform];
30
- const arch = archMap[process.arch];
31
- if (!osName || !arch) {
32
- throw new Error(`unsupported platform: ${process.platform}/${process.arch}`);
33
- }
14
+ const { version } = require(path.join(ROOT, 'package.json'));
34
15
 
35
- return { osName, arch };
36
- }
16
+ const platformMap = { linux: 'linux', darwin: 'darwin', win32: 'windows' };
17
+ const archMap = { x64: 'amd64', arm64: 'arm64' };
37
18
 
38
- function download(url, outFile) {
39
- return new Promise((resolve, reject) => {
40
- const req = https.get(url, (res) => {
41
- if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
42
- res.resume();
43
- return resolve(download(res.headers.location, outFile));
44
- }
45
-
46
- if (res.statusCode !== 200) {
47
- res.resume();
48
- return reject(new Error(`download failed (${res.statusCode}): ${url}`));
49
- }
50
-
51
- const file = fs.createWriteStream(outFile);
52
- res.pipe(file);
53
- file.on('finish', () => file.close(() => resolve()));
54
- file.on('error', reject);
55
- });
56
- req.on('error', reject);
57
- });
19
+ const osPlatform = platformMap[process.platform];
20
+ const osArch = archMap[process.arch];
21
+
22
+ if (!osPlatform || !osArch) {
23
+ console.error(`Unsupported platform: ${process.platform}/${process.arch}`);
24
+ process.exit(1);
58
25
  }
59
26
 
60
- function downloadText(url) {
27
+ const tag = `v${version}`;
28
+ const base = `https://github.com/${OWNER}/${PROJECT}/releases/download/${tag}`;
29
+ const archiveName = `${PROJECT}_${osPlatform}_${osArch}.tar.gz`;
30
+ const binName = process.platform === 'win32' ? `${PROJECT}.exe` : PROJECT;
31
+ const destPath = path.join(VENDOR_DIR, binName);
32
+
33
+ function download(url, dest) {
61
34
  return new Promise((resolve, reject) => {
62
- const req = https.get(url, (res) => {
63
- if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
64
- res.resume();
65
- return resolve(downloadText(res.headers.location));
66
- }
67
-
68
- if (res.statusCode !== 200) {
69
- res.resume();
70
- return reject(new Error(`download failed (${res.statusCode}): ${url}`));
71
- }
72
-
73
- let data = '';
74
- res.setEncoding('utf8');
75
- res.on('data', (chunk) => {
76
- data += chunk;
77
- });
78
- res.on('end', () => resolve(data));
79
- res.on('error', reject);
80
- });
81
- req.on('error', reject);
35
+ const file = fs.createWriteStream(dest);
36
+ function get(url) {
37
+ https.get(url, (res) => {
38
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
39
+ return get(res.headers.location);
40
+ }
41
+ if (res.statusCode !== 200) {
42
+ return reject(new Error(`HTTP ${res.statusCode} downloading ${url}`));
43
+ }
44
+ res.pipe(file);
45
+ file.on('finish', () => file.close(resolve));
46
+ file.on('error', reject);
47
+ }).on('error', reject);
48
+ }
49
+ get(url);
82
50
  });
83
51
  }
84
52
 
85
- function parseChecksums(text) {
86
- const checksums = new Map();
87
- for (const line of text.split(/\r?\n/)) {
88
- const trimmed = line.trim();
89
- if (!trimmed || trimmed.startsWith('#')) {
90
- continue;
91
- }
92
-
93
- const match = trimmed.match(/^([a-fA-F0-9]{64})\s+\*?(.+)$/);
94
- if (!match) {
95
- continue;
53
+ function fetch(url) {
54
+ return new Promise((resolve, reject) => {
55
+ function get(url) {
56
+ https.get(url, (res) => {
57
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
58
+ return get(res.headers.location);
59
+ }
60
+ if (res.statusCode !== 200) {
61
+ return reject(new Error(`HTTP ${res.statusCode} fetching ${url}`));
62
+ }
63
+ const chunks = [];
64
+ res.on('data', c => chunks.push(c));
65
+ res.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
66
+ res.on('error', reject);
67
+ }).on('error', reject);
96
68
  }
97
- checksums.set(match[2].trim(), match[1].toLowerCase());
98
- }
99
- return checksums;
69
+ get(url);
70
+ });
100
71
  }
101
72
 
102
73
  function sha256File(filePath) {
103
- const hash = crypto.createHash('sha256');
104
- hash.update(fs.readFileSync(filePath));
105
- return hash.digest('hex');
106
- }
107
-
108
- async function verifyArchiveChecksum(urlBase, archivePath, archiveName, version) {
109
- const checksumCandidates = [
110
- 'checksums.txt',
111
- `${PROJECT}_${version}_checksums.txt`,
112
- ];
113
-
114
- let checksumFile = '';
115
- let checksumName = '';
116
- for (const candidate of checksumCandidates) {
117
- try {
118
- checksumFile = await downloadText(`${urlBase}/${candidate}`);
119
- checksumName = candidate;
120
- break;
121
- } catch (err) {
122
- if (!String(err.message).includes('download failed (404)')) {
123
- throw err;
124
- }
125
- }
126
- }
127
-
128
- if (!checksumFile) {
129
- throw new Error(`no checksum file found (tried: ${checksumCandidates.join(', ')})`);
130
- }
131
-
132
- const checksums = parseChecksums(checksumFile);
133
- const expected = checksums.get(archiveName);
134
- if (!expected) {
135
- throw new Error(`checksum for ${archiveName} not found in ${checksumName}`);
136
- }
137
-
138
- const actual = sha256File(archivePath);
139
- if (actual !== expected) {
140
- throw new Error(`checksum mismatch for ${archiveName}`);
141
- }
74
+ return new Promise((resolve, reject) => {
75
+ const hash = crypto.createHash('sha256');
76
+ fs.createReadStream(filePath)
77
+ .on('data', d => hash.update(d))
78
+ .on('end', () => resolve(hash.digest('hex')))
79
+ .on('error', reject);
80
+ });
142
81
  }
143
82
 
144
- function run(cmd, args) {
145
- const result = spawnSync(cmd, args, { stdio: 'inherit' });
146
- if (result.status !== 0) {
147
- throw new Error(`command failed: ${cmd} ${args.join(' ')}`);
83
+ function parseChecksum(checksumsTxt, filename) {
84
+ for (const line of checksumsTxt.split('\n')) {
85
+ const [hash, name] = line.trim().split(/\s+/);
86
+ if (name === filename) return hash;
148
87
  }
88
+ return null;
149
89
  }
150
90
 
151
- function installFromArchive(archivePath, osName) {
91
+ async function main() {
92
+ console.log(`Installing ${PROJECT} ${version} for ${osPlatform}/${osArch}...`);
152
93
  fs.mkdirSync(VENDOR_DIR, { recursive: true });
153
94
 
154
- const binName = osName === 'windows' ? `${PROJECT}.exe` : PROJECT;
155
- const destBinary = path.join(VENDOR_DIR, binName);
156
-
157
- if (archivePath.endsWith('.zip')) {
158
- if (process.platform === 'win32') {
159
- run('powershell', [
160
- '-NoProfile',
161
- '-Command',
162
- `Expand-Archive -Path \"${archivePath}\" -DestinationPath \"${VENDOR_DIR}\" -Force`,
163
- ]);
164
- } else {
165
- run('unzip', ['-o', archivePath, '-d', VENDOR_DIR]);
166
- }
167
- } else {
168
- run('tar', ['-xzf', archivePath, '-C', VENDOR_DIR]);
95
+ const checksumsTxt = await fetch(`${base}/checksums.txt`);
96
+ const expectedHash = parseChecksum(checksumsTxt, archiveName);
97
+ if (!expectedHash) {
98
+ throw new Error(`No checksum found for ${archiveName} in checksums.txt`);
169
99
  }
170
100
 
171
- if (!fs.existsSync(destBinary)) {
172
- throw new Error(`binary not found after extraction: ${destBinary}`);
173
- }
174
-
175
- if (process.platform !== 'win32') {
176
- fs.chmodSync(destBinary, 0o755);
177
- }
178
- }
101
+ const tmpFile = path.join(os.tmpdir(), archiveName);
102
+ try {
103
+ await download(`${base}/${archiveName}`, tmpFile);
179
104
 
180
- async function main() {
181
- const { osName, arch } = mapPlatform();
182
- const pkgVersion = PACKAGE_JSON.version.replace(/^v/, '');
183
- const tag = `v${pkgVersion}`;
184
- const assetBase = `${PROJECT}_${osName}_${arch}`;
185
- const ext = 'tar.gz';
186
- const asset = `${assetBase}.${ext}`;
187
- const urlBase = `https://github.com/${OWNER}/${REPO}/releases/download/${tag}`;
188
- const url = `${urlBase}/${asset}`;
189
-
190
- const cacheDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nibble-npm-'));
191
- const archivePath = path.join(cacheDir, asset);
105
+ const actualHash = await sha256File(tmpFile);
106
+ if (actualHash !== expectedHash) {
107
+ throw new Error(`Checksum mismatch for ${archiveName}: expected ${expectedHash}, got ${actualHash}`);
108
+ }
192
109
 
193
- try {
194
- console.log(`Downloading ${asset} from ${url}`);
195
- await download(url, archivePath);
196
- await verifyArchiveChecksum(urlBase, archivePath, asset, pkgVersion);
197
- installFromArchive(archivePath, osName);
198
- console.log('Installed nibble binary successfully');
110
+ await tar.extract({ file: tmpFile, cwd: VENDOR_DIR, filter: p => p === binName });
111
+ console.log(`Installed ${PROJECT} to ${destPath}`);
199
112
  } finally {
200
- if (fs.rmSync) {
201
- fs.rmSync(cacheDir, { recursive: true, force: true });
202
- } else {
203
- fs.rmdirSync(cacheDir, { recursive: true });
204
- }
113
+ try { fs.unlinkSync(tmpFile); } catch {}
205
114
  }
206
115
  }
207
116
 
208
117
  main().catch((err) => {
209
- console.error(`nibble install failed: ${err.message}`);
118
+ console.error(`${PROJECT} install failed: ${err.message}`);
210
119
  process.exit(1);
211
120
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backendsystems/nibble",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
4
4
  "description": "Fast local network scanner with hardware identification and a terminal UI",
5
5
  "bin": {
6
6
  "nibble": "bin/nibble.js"
@@ -8,6 +8,9 @@
8
8
  "scripts": {
9
9
  "postinstall": "node bin/install.js"
10
10
  },
11
+ "dependencies": {
12
+ "tar": "^7.0.0"
13
+ },
11
14
  "repository": {
12
15
  "type": "git",
13
16
  "url": "https://github.com/backendsystems/nibble"