xpose-cli 0.1.0 → 0.3.2
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 +1 -1
- package/bin/install.js +114 -22
- package/bin/test-install.js +37 -0
- package/package.json +8 -4
package/LICENSE
CHANGED
package/bin/install.js
CHANGED
|
@@ -4,14 +4,21 @@ const fs = require('fs');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const os = require('os');
|
|
6
6
|
const https = require('https');
|
|
7
|
+
const crypto = require('crypto');
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* xpose binary downloader
|
|
10
11
|
* Fetches the pre-compiled Rust binary for the current platform.
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
const
|
|
14
|
+
function getPackageVersion() {
|
|
15
|
+
const packageJsonPath = path.join(__dirname, '..', 'package.json');
|
|
16
|
+
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
17
|
+
return pkg.version;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const VERSION = getPackageVersion();
|
|
21
|
+
const REPO = 'vkaylee/xpose-cli';
|
|
15
22
|
const BASE_URL = `https://github.com/${REPO}/releases/download/v${VERSION}`;
|
|
16
23
|
|
|
17
24
|
const BIN_DIR = path.join(os.homedir(), '.xpose', 'bin');
|
|
@@ -38,42 +45,127 @@ function getReleaseName() {
|
|
|
38
45
|
return `xpose-${target}.tar.gz`;
|
|
39
46
|
}
|
|
40
47
|
|
|
48
|
+
function fetch(url) {
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
https.get(url, (res) => {
|
|
51
|
+
if (res.statusCode === 301 || res.statusCode === 302) {
|
|
52
|
+
return fetch(res.headers.location).then(resolve).catch(reject);
|
|
53
|
+
}
|
|
54
|
+
if (res.statusCode !== 200) {
|
|
55
|
+
return reject(new Error(`Failed to fetch ${url} (Status: ${res.statusCode})`));
|
|
56
|
+
}
|
|
57
|
+
resolve(res);
|
|
58
|
+
}).on('error', reject);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function getFileContent(url) {
|
|
63
|
+
return new Promise((resolve, reject) => {
|
|
64
|
+
https.get(url, (res) => {
|
|
65
|
+
if (res.statusCode === 301 || res.statusCode === 302) {
|
|
66
|
+
return getFileContent(res.headers.location).then(resolve).catch(reject);
|
|
67
|
+
}
|
|
68
|
+
let data = '';
|
|
69
|
+
res.on('data', hunk => data += hunk);
|
|
70
|
+
res.on('end', () => resolve(data.trim()));
|
|
71
|
+
res.on('error', reject);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
41
76
|
async function download() {
|
|
77
|
+
let cliProgress;
|
|
78
|
+
try {
|
|
79
|
+
cliProgress = require('cli-progress');
|
|
80
|
+
} catch (e) {
|
|
81
|
+
console.log('Progress bar library not found, skipping visual progress.');
|
|
82
|
+
}
|
|
83
|
+
|
|
42
84
|
if (!fs.existsSync(BIN_DIR)) {
|
|
43
85
|
fs.mkdirSync(BIN_DIR, { recursive: true });
|
|
44
86
|
}
|
|
45
87
|
|
|
46
88
|
const releaseName = getReleaseName();
|
|
47
89
|
const url = `${BASE_URL}/${releaseName}`;
|
|
90
|
+
const checksumUrl = `${url}.sha256`;
|
|
48
91
|
const dest = path.join(BIN_DIR, releaseName);
|
|
49
92
|
|
|
50
|
-
|
|
93
|
+
try {
|
|
94
|
+
console.log(`Verifying release for ${releaseName}...`);
|
|
51
95
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
console.
|
|
58
|
-
return;
|
|
96
|
+
let expectedChecksum;
|
|
97
|
+
try {
|
|
98
|
+
const checksumContent = await getFileContent(checksumUrl);
|
|
99
|
+
expectedChecksum = checksumContent.split(' ')[0];
|
|
100
|
+
} catch (e) {
|
|
101
|
+
console.warn(`⚠️ Checksum file not found at ${checksumUrl}. Skipping verification.`);
|
|
59
102
|
}
|
|
60
103
|
|
|
104
|
+
console.log(`Downloading xpose binary...`);
|
|
105
|
+
const response = await fetch(url);
|
|
106
|
+
const totalSize = parseInt(response.headers['content-length'], 10);
|
|
107
|
+
|
|
108
|
+
const file = fs.createWriteStream(dest);
|
|
109
|
+
const hash = crypto.createHash('sha256');
|
|
110
|
+
|
|
111
|
+
const progressBar = cliProgress ? new cliProgress.SingleBar({
|
|
112
|
+
format: 'Progress |{bar}| {percentage}% | {value}/{total} bytes',
|
|
113
|
+
barCompleteChar: '\u2588',
|
|
114
|
+
barIncompleteChar: '\u2591',
|
|
115
|
+
hideCursor: true
|
|
116
|
+
}) : null;
|
|
117
|
+
|
|
118
|
+
if (progressBar) progressBar.start(totalSize, 0);
|
|
119
|
+
|
|
120
|
+
let downloadedSize = 0;
|
|
121
|
+
response.on('data', (chunk) => {
|
|
122
|
+
downloadedSize += chunk.length;
|
|
123
|
+
if (progressBar) progressBar.update(downloadedSize);
|
|
124
|
+
hash.update(chunk);
|
|
125
|
+
});
|
|
126
|
+
|
|
61
127
|
response.pipe(file);
|
|
62
128
|
|
|
63
|
-
|
|
64
|
-
file.
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
129
|
+
await new Promise((resolve, reject) => {
|
|
130
|
+
file.on('finish', () => {
|
|
131
|
+
if (progressBar) progressBar.stop();
|
|
132
|
+
file.close();
|
|
133
|
+
resolve();
|
|
134
|
+
});
|
|
135
|
+
file.on('error', reject);
|
|
68
136
|
});
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
137
|
+
|
|
138
|
+
if (expectedChecksum) {
|
|
139
|
+
const actualChecksum = hash.digest('hex');
|
|
140
|
+
if (actualChecksum === expectedChecksum) {
|
|
141
|
+
console.log('✅ Integrity verified: SHA256 matches.');
|
|
142
|
+
} else {
|
|
143
|
+
console.error('❌ Integrity check FAILED: Checksum mismatch!');
|
|
144
|
+
fs.unlinkSync(dest);
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
console.log(`Successfully installed to ${dest}`);
|
|
150
|
+
console.log('Note: Run "xpose" to start the tunnel.');
|
|
151
|
+
|
|
152
|
+
} catch (err) {
|
|
153
|
+
if (err.message.includes('404')) {
|
|
154
|
+
console.warn(`Binary not yet published to GitHub Releases.`);
|
|
155
|
+
console.log(`Skipping auto-download. You can build manually: cargo build --release`);
|
|
156
|
+
} else {
|
|
157
|
+
console.error(`Download failed: ${err.message}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
72
160
|
}
|
|
73
161
|
|
|
74
162
|
// Only download if being installed via NPM (not in local dev)
|
|
75
|
-
if (
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
163
|
+
if (require.main === module) {
|
|
164
|
+
if (!process.env.XPOSE_DEV) {
|
|
165
|
+
download();
|
|
166
|
+
} else {
|
|
167
|
+
console.log('XPOSE_DEV detected, skipping download.');
|
|
168
|
+
}
|
|
79
169
|
}
|
|
170
|
+
|
|
171
|
+
module.exports = { download, getReleaseName };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const assert = require('assert');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const crypto = require('crypto');
|
|
5
|
+
const http = require('http'); // We'll use this to mock
|
|
6
|
+
|
|
7
|
+
// Mocking dependencies would be complex without a library like proxyquire or jest.
|
|
8
|
+
// For a simple standalone test, we'll use environment variables and temporary paths.
|
|
9
|
+
|
|
10
|
+
const { download } = require('./install');
|
|
11
|
+
|
|
12
|
+
async function testSuccessfulDownload() {
|
|
13
|
+
console.log('Running testSuccessfulDownload...');
|
|
14
|
+
// This is a minimal test to ensure the script doesn't crash
|
|
15
|
+
// and correctly identifies dev environment.
|
|
16
|
+
process.env.XPOSE_DEV = '1';
|
|
17
|
+
try {
|
|
18
|
+
await download();
|
|
19
|
+
console.log('✅ testSuccessfulDownload passed (dev skip)');
|
|
20
|
+
} catch (e) {
|
|
21
|
+
console.error('❌ testSuccessfulDownload failed:', e);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// In a real scenario, we would mock https.get and fs.createWriteStream.
|
|
27
|
+
// Since we want to stay within standard node tools for now, we'll verify the logic structure.
|
|
28
|
+
|
|
29
|
+
async function runTests() {
|
|
30
|
+
await testSuccessfulDownload();
|
|
31
|
+
console.log('\nAll tests passed!');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
runTests().catch(err => {
|
|
35
|
+
console.error(err);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xpose-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "Cloudflare Tunnel CLI for developers - Vivid TUI, auto-copy, and smart defaults.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"xpose": "bin/cli.js"
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
],
|
|
12
12
|
"repository": {
|
|
13
13
|
"type": "git",
|
|
14
|
-
"url": "https://github.com/
|
|
14
|
+
"url": "https://github.com/vkaylee/xpose-cli"
|
|
15
15
|
},
|
|
16
16
|
"keywords": [
|
|
17
17
|
"cloudflare",
|
|
@@ -24,9 +24,13 @@
|
|
|
24
24
|
"author": "",
|
|
25
25
|
"license": "MIT",
|
|
26
26
|
"scripts": {
|
|
27
|
-
"postinstall": "node bin/install.js"
|
|
27
|
+
"postinstall": "node bin/install.js",
|
|
28
|
+
"test:install": "node bin/test-install.js"
|
|
28
29
|
},
|
|
29
30
|
"engines": {
|
|
30
31
|
"node": ">=16.0.0"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"cli-progress": "^3.12.0"
|
|
31
35
|
}
|
|
32
|
-
}
|
|
36
|
+
}
|