@aweray/hsk-cli 0.2.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.
@@ -0,0 +1,112 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+
5
+ /**
6
+ * 查找项目根目录(向上查找 package.json)
7
+ */
8
+ function findProjectRoot(cwd = process.cwd()) {
9
+ let current = cwd;
10
+ while (current !== path.dirname(current)) {
11
+ if (fs.existsSync(path.join(current, 'package.json'))) {
12
+ return current;
13
+ }
14
+ current = path.dirname(current);
15
+ }
16
+ return null;
17
+ }
18
+
19
+ /**
20
+ * 获取资源文件路径(项目绑定,找不到项目则回退到全局)
21
+ */
22
+ function getResourceFilePath() {
23
+ const projectRoot = findProjectRoot();
24
+ if (projectRoot) {
25
+ return path.join(projectRoot, '.hsk', 'resources.json');
26
+ }
27
+ return path.join(os.homedir(), '.hsk', 'resources.json');
28
+ }
29
+
30
+ function ensureDir(filePath) {
31
+ const dir = path.dirname(filePath);
32
+ if (!fs.existsSync(dir)) {
33
+ fs.mkdirSync(dir, { recursive: true });
34
+ }
35
+ }
36
+
37
+ function readStore() {
38
+ const filePath = getResourceFilePath();
39
+ if (!fs.existsSync(filePath)) {
40
+ return { version: 1, resources: [] };
41
+ }
42
+ try {
43
+ const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
44
+ return data || { version: 1, resources: [] };
45
+ } catch (e) {
46
+ return { version: 1, resources: [] };
47
+ }
48
+ }
49
+
50
+ function writeStore(data) {
51
+ const filePath = getResourceFilePath();
52
+ ensureDir(filePath);
53
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
54
+ try {
55
+ fs.chmodSync(filePath, 0o600);
56
+ } catch (e) {}
57
+ }
58
+
59
+ function findResource(predicate) {
60
+ const store = readStore();
61
+ return store.resources.find(predicate) || null;
62
+ }
63
+
64
+ function findTunnel(ip, port) {
65
+ return findResource(
66
+ (r) => r.type === 'tunnel' && r.localIp === ip && r.localPort === port
67
+ );
68
+ }
69
+
70
+ function saveResource(resource) {
71
+ const store = readStore();
72
+ const index = store.resources.findIndex((r) => r.id === resource.id);
73
+ if (index >= 0) {
74
+ store.resources[index] = { ...store.resources[index], ...resource };
75
+ } else {
76
+ store.resources.push(resource);
77
+ }
78
+ writeStore(store);
79
+ }
80
+
81
+ function updateResourceStatus(id, status) {
82
+ const store = readStore();
83
+ const resource = store.resources.find((r) => r.id === id);
84
+ if (resource) {
85
+ resource.status = status;
86
+ resource.lastCheckedAt = new Date().toISOString();
87
+ writeStore(store);
88
+ }
89
+ }
90
+
91
+ function removeResource(id) {
92
+ const store = readStore();
93
+ store.resources = store.resources.filter((r) => r.id !== id);
94
+ writeStore(store);
95
+ }
96
+
97
+ function getAllResources() {
98
+ return readStore().resources;
99
+ }
100
+
101
+ module.exports = {
102
+ findProjectRoot,
103
+ getResourceFilePath,
104
+ readStore,
105
+ writeStore,
106
+ findResource,
107
+ findTunnel,
108
+ saveResource,
109
+ updateResourceStatus,
110
+ removeResource,
111
+ getAllResources,
112
+ };
package/lib/tunnel.js ADDED
@@ -0,0 +1,192 @@
1
+ const { spawn } = require('child_process');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const chalk = require('chalk');
5
+ const download = require('./download');
6
+ const platform = require('./platform');
7
+ const pidManager = require('./pidManager');
8
+ const resourceStore = require('./resourceStore');
9
+ const resourceChecker = require('./resourceChecker');
10
+
11
+ async function start(options) {
12
+ const { ip, port, arch, detached = false, reuse = false } = options;
13
+
14
+ const portNum = Number(port);
15
+ if (isNaN(portNum) || portNum < 1 || portNum > 65535) {
16
+ throw new Error('端口号必须是 1-65535 之间的有效数字');
17
+ }
18
+
19
+ const resourceId = `tunnel-${ip}-${portNum}`;
20
+
21
+ // === --reuse: 检查是否已有活跃隧道 ===
22
+ if (reuse) {
23
+ const existing = resourceStore.findTunnel(ip, portNum);
24
+ if (existing) {
25
+ const check = await resourceChecker.checkResource(existing);
26
+ if (check.valid) {
27
+ return {
28
+ success: true,
29
+ publicUrl: existing.publicUrl,
30
+ ip,
31
+ port: portNum,
32
+ binary: path.basename(existing.binary || ''),
33
+ pid: existing.pid,
34
+ detached: existing.detached,
35
+ reused: true,
36
+ };
37
+ }
38
+ // 失效则删除旧记录
39
+ resourceStore.removeResource(existing.id);
40
+ }
41
+ }
42
+
43
+ const binPath = await download.ensureBinary(arch);
44
+ const args = ['-ip', ip, '-port', String(portNum)];
45
+
46
+ if (detached) {
47
+ const logFile = pidManager.getLogFilePath('tunnel', { ip, port: portNum });
48
+ const out = fs.openSync(logFile, 'a');
49
+ const err = fs.openSync(logFile, 'a');
50
+
51
+ const proc = spawn(binPath, args, {
52
+ stdio: ['ignore', out, err],
53
+ detached: true,
54
+ windowsHide: true
55
+ });
56
+
57
+ proc.unref();
58
+ fs.closeSync(out);
59
+ fs.closeSync(err);
60
+
61
+ pidManager.savePid('tunnel', { ip, port: portNum }, proc.pid);
62
+
63
+ resourceStore.saveResource({
64
+ id: resourceId,
65
+ type: 'tunnel',
66
+ localIp: ip,
67
+ localPort: portNum,
68
+ publicUrl: null,
69
+ pid: proc.pid,
70
+ binary: path.basename(binPath),
71
+ detached: true,
72
+ logFile,
73
+ status: 'running',
74
+ createdAt: new Date().toISOString(),
75
+ lastCheckedAt: new Date().toISOString(),
76
+ });
77
+
78
+ return {
79
+ success: true,
80
+ publicUrl: null,
81
+ ip,
82
+ port: portNum,
83
+ binary: path.basename(binPath),
84
+ pid: proc.pid,
85
+ detached: true,
86
+ logFile,
87
+ reused: false,
88
+ };
89
+ }
90
+
91
+ return new Promise((resolve, reject) => {
92
+ const proc = spawn(binPath, args, {
93
+ stdio: ['ignore', 'pipe', 'pipe'],
94
+ windowsHide: true
95
+ });
96
+
97
+ let stdout = '';
98
+ let stderr = '';
99
+ let urlCaptured = null;
100
+ let timeoutId;
101
+ let isResolved = false;
102
+
103
+ proc.stdout.on('data', (data) => {
104
+ const chunk = data.toString();
105
+ stdout += chunk;
106
+ process.stdout.write(chunk);
107
+
108
+ if (!urlCaptured) {
109
+ const urlMatch = chunk.match(/(https?:\/\/[^\s\n]+)/);
110
+ if (urlMatch) {
111
+ urlCaptured = urlMatch[1];
112
+ clearTimeout(timeoutId);
113
+ isResolved = true;
114
+
115
+ resourceStore.saveResource({
116
+ id: resourceId,
117
+ type: 'tunnel',
118
+ localIp: ip,
119
+ localPort: portNum,
120
+ publicUrl: urlCaptured,
121
+ pid: proc.pid,
122
+ binary: path.basename(binPath),
123
+ detached: false,
124
+ status: 'running',
125
+ createdAt: new Date().toISOString(),
126
+ lastCheckedAt: new Date().toISOString(),
127
+ });
128
+
129
+ resolve({
130
+ success: true,
131
+ publicUrl: urlCaptured,
132
+ ip,
133
+ port: portNum,
134
+ binary: path.basename(binPath),
135
+ pid: proc.pid,
136
+ detached: false,
137
+ reused: false,
138
+ stdout: stdout.trim(),
139
+ stderr: stderr.trim()
140
+ });
141
+ }
142
+ }
143
+ });
144
+
145
+ proc.stderr.on('data', (data) => {
146
+ const chunk = data.toString();
147
+ stderr += chunk;
148
+ process.stderr.write(chunk);
149
+ });
150
+
151
+ timeoutId = setTimeout(() => {
152
+ if (!isResolved) {
153
+ proc.kill();
154
+ reject(new Error('启动超时(30秒),请检查网络连接或本地服务是否可用。\n输出: ' + stdout + '\n错误: ' + stderr));
155
+ }
156
+ }, 30000);
157
+
158
+ proc.on('error', (err) => {
159
+ clearTimeout(timeoutId);
160
+ if (!isResolved) {
161
+ reject(new Error(`无法启动客户端: ${err.message}`));
162
+ }
163
+ });
164
+
165
+ proc.on('close', (code) => {
166
+ clearTimeout(timeoutId);
167
+ if (!isResolved) {
168
+ if (code !== 0) {
169
+ reject(new Error(`客户端异常退出 (code: ${code})\n输出: ${stdout}\n错误: ${stderr}`));
170
+ } else {
171
+ reject(new Error('未能从输出中解析到公网地址。\n输出: ' + stdout));
172
+ }
173
+ } else {
174
+ console.log('\n' + chalk.yellow('⚠️ 隧道已断开(客户端进程退出)'));
175
+ resourceStore.updateResourceStatus(resourceId, 'stopped');
176
+ process.exit(0);
177
+ }
178
+ });
179
+
180
+ process.on('SIGINT', () => {
181
+ console.log('\n' + '⏹️ 正在停止隧道...');
182
+ try { proc.kill('SIGTERM'); } catch (e) {}
183
+ setTimeout(() => { try { proc.kill('SIGKILL'); } catch (e) {} }, 2000);
184
+ });
185
+
186
+ process.on('SIGTERM', () => {
187
+ try { proc.kill('SIGTERM'); } catch (e) {}
188
+ });
189
+ });
190
+ }
191
+
192
+ module.exports = { start };
package/lib/version.js ADDED
@@ -0,0 +1,3 @@
1
+ const { version } = require('../package.json');
2
+
3
+ module.exports = version;
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@aweray/hsk-cli",
3
+ "version": "0.2.2",
4
+ "description": "HSK CLI - 内网穿透 & 文件托管,支持 Windows/macOS/Linux 多平台",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "hsk-cli": "bin/hsk-cli.js"
8
+ },
9
+ "scripts": {
10
+ "test": "echo \"Tests not implemented yet\" && exit 0"
11
+ },
12
+ "keywords": [
13
+ "hsk",
14
+ "tunnel",
15
+ "nat-traversal",
16
+ "file-hosting",
17
+ "cli",
18
+ "内网穿透",
19
+ "文件托管",
20
+ "ai-agent",
21
+ "remote-access"
22
+ ],
23
+ "author": "HSK Team",
24
+ "license": "MIT",
25
+ "engines": {
26
+ "node": ">=14.0.0"
27
+ },
28
+ "dependencies": {
29
+ "commander": "^11.0.0",
30
+ "chalk": "^4.1.2",
31
+ "node-fetch": "^2.7.0",
32
+ "form-data": "^4.0.0"
33
+ },
34
+ "homepage": "https://hsk.oray.com",
35
+ "files": [
36
+ "bin/",
37
+ "lib/",
38
+ "index.js",
39
+ "README.md",
40
+ "versions.json"
41
+ ]
42
+ }
package/versions.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "version": "0.2.2",
3
+ "downloadBaseUrl": "https://dw.oray.com/onion/cli",
4
+ "binaries": {
5
+ "windows-amd64": {
6
+ "filename": "hsk-cli-windows-amd64-v0.2.2.exe"
7
+ },
8
+ "darwin-amd64": {
9
+ "filename": "hsk-cli-darwin-amd64-v0.2.2"
10
+ },
11
+ "darwin-arm64": {
12
+ "filename": "hsk-cli-darwin-arm64-v0.2.2"
13
+ },
14
+ "linux-amd64": {
15
+ "filename": "hsk-cli-linux-amd64-v0.2.2"
16
+ }
17
+ }
18
+ }