@vino.tian/myte 0.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/LICENSE +1 -0
- package/README.md +3 -0
- package/bin/myte.js +41 -0
- package/package.json +22 -0
- package/scripts/install.js +191 -0
package/LICENSE
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
123
|
package/README.md
ADDED
package/bin/myte.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { spawn } = require("node:child_process");
|
|
4
|
+
const { existsSync } = require("node:fs");
|
|
5
|
+
const { join } = require("node:path");
|
|
6
|
+
|
|
7
|
+
function detectTarget() {
|
|
8
|
+
if (process.platform === "linux" && process.arch === "x64") {
|
|
9
|
+
return { key: "linux-x64", binary: "myte" };
|
|
10
|
+
}
|
|
11
|
+
if (process.platform === "linux" && process.arch === "arm64") {
|
|
12
|
+
return { key: "linux-arm64", binary: "myte" };
|
|
13
|
+
}
|
|
14
|
+
if (process.platform === "win32" && process.arch === "x64") {
|
|
15
|
+
return { key: "win32-x64", binary: "myte.exe" };
|
|
16
|
+
}
|
|
17
|
+
if (process.platform === "darwin" && process.arch === "arm64") {
|
|
18
|
+
return { key: "darwin-arm64", binary: "myte" };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
console.error(`暂不支持的平台:${process.platform}-${process.arch}`);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const target = detectTarget();
|
|
26
|
+
const binaryPath = join(__dirname, "..", "vendor", target.key, target.binary);
|
|
27
|
+
|
|
28
|
+
if (!existsSync(binaryPath)) {
|
|
29
|
+
console.error(`缺少当前平台的 myte 二进制:${binaryPath}`);
|
|
30
|
+
console.error("请重新安装 @vino.tian/myte,或检查 postinstall 是否成功。");
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const child = spawn(binaryPath, process.argv.slice(2), { stdio: "inherit" });
|
|
35
|
+
child.on("exit", (code, signal) => {
|
|
36
|
+
if (signal) {
|
|
37
|
+
process.kill(process.pid, signal);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
process.exit(code ?? 0);
|
|
41
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vino.tian/myte",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "myte CLI",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"bin": {
|
|
7
|
+
"myte": "bin/myte.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"scripts",
|
|
12
|
+
"vendor",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"postinstall": "node ./scripts/install.js"
|
|
17
|
+
},
|
|
18
|
+
"myte": {
|
|
19
|
+
"packageKey": "myte",
|
|
20
|
+
"releaseRepository": "tianweilong/deploy-center"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { createHash } = require("node:crypto");
|
|
4
|
+
const { createWriteStream, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync } = require("node:fs");
|
|
5
|
+
const { tmpdir } = require("node:os");
|
|
6
|
+
const { dirname, join } = require("node:path");
|
|
7
|
+
const { pipeline } = require("node:stream/promises");
|
|
8
|
+
const { spawnSync } = require("node:child_process");
|
|
9
|
+
const https = require("node:https");
|
|
10
|
+
const SEMVER_VERSION = /^\d+\.\d+\.\d+$/;
|
|
11
|
+
|
|
12
|
+
function detectTarget() {
|
|
13
|
+
if (process.platform === "linux" && process.arch === "x64") {
|
|
14
|
+
return { key: "linux-x64", assetExt: "tar.gz", binary: "myte" };
|
|
15
|
+
}
|
|
16
|
+
if (process.platform === "linux" && process.arch === "arm64") {
|
|
17
|
+
return { key: "linux-arm64", assetExt: "tar.gz", binary: "myte" };
|
|
18
|
+
}
|
|
19
|
+
if (process.platform === "win32" && process.arch === "x64") {
|
|
20
|
+
return { key: "win32-x64", assetExt: "zip", binary: "myte.exe" };
|
|
21
|
+
}
|
|
22
|
+
if (process.platform === "darwin" && process.arch === "arm64") {
|
|
23
|
+
return { key: "darwin-arm64", assetExt: "tar.gz", binary: "myte" };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
throw new Error(`暂不支持的平台:${process.platform}-${process.arch}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function packageMetadata() {
|
|
30
|
+
return require(join(__dirname, "..", "package.json"));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function validatePackageVersion(version) {
|
|
34
|
+
if (version === "0.0.0") {
|
|
35
|
+
throw new Error("检测到占位版本 0.0.0,说明发布流程未注入 npm 实际版本。");
|
|
36
|
+
}
|
|
37
|
+
if (!SEMVER_VERSION.test(version)) {
|
|
38
|
+
throw new Error(`检测到非法 npm 版本:${version}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function vendorBinaryPath(target) {
|
|
43
|
+
return join(__dirname, "..", "vendor", target.key, target.binary);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function fetchBuffer(url) {
|
|
47
|
+
return new Promise((resolve, reject) => {
|
|
48
|
+
const request = https.get(url, (response) => {
|
|
49
|
+
if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
|
|
50
|
+
response.resume();
|
|
51
|
+
resolve(fetchBuffer(response.headers.location));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (response.statusCode !== 200) {
|
|
55
|
+
reject(new Error(`下载失败:${url}(HTTP ${response.statusCode})`));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const chunks = [];
|
|
59
|
+
response.on("data", (chunk) => chunks.push(chunk));
|
|
60
|
+
response.on("end", () => resolve(Buffer.concat(chunks)));
|
|
61
|
+
});
|
|
62
|
+
request.on("error", reject);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function downloadFile(url, filePath) {
|
|
67
|
+
await new Promise((resolve, reject) => {
|
|
68
|
+
const request = https.get(url, (response) => {
|
|
69
|
+
if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
|
|
70
|
+
response.resume();
|
|
71
|
+
downloadFile(response.headers.location, filePath).then(resolve, reject);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (response.statusCode !== 200) {
|
|
75
|
+
reject(new Error(`下载失败:${url}(HTTP ${response.statusCode})`));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
pipeline(response, createWriteStream(filePath)).then(resolve, reject);
|
|
79
|
+
});
|
|
80
|
+
request.on("error", reject);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function sha256(filePath) {
|
|
85
|
+
const content = readFileSync(filePath);
|
|
86
|
+
return createHash("sha256").update(content).digest("hex");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function verifyChecksum(checksumsContent, assetName, filePath) {
|
|
90
|
+
const line = checksumsContent
|
|
91
|
+
.split(/\r?\n/)
|
|
92
|
+
.find((entry) => entry.trim().endsWith(` ${assetName}`));
|
|
93
|
+
if (!line) {
|
|
94
|
+
throw new Error(`校验文件中缺少 ${assetName}`);
|
|
95
|
+
}
|
|
96
|
+
const expected = line.trim().split(/\s+/)[0];
|
|
97
|
+
const actual = sha256(filePath);
|
|
98
|
+
if (expected !== actual) {
|
|
99
|
+
throw new Error(`校验失败:${assetName} 期望 ${expected},实际 ${actual}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function extractArchive(filePath, outputDir) {
|
|
104
|
+
mkdirSync(outputDir, { recursive: true });
|
|
105
|
+
|
|
106
|
+
if (process.platform === "win32") {
|
|
107
|
+
const powershellCommands = [
|
|
108
|
+
{
|
|
109
|
+
command: "powershell",
|
|
110
|
+
args: [
|
|
111
|
+
"-NoLogo",
|
|
112
|
+
"-NoProfile",
|
|
113
|
+
"-NonInteractive",
|
|
114
|
+
"-Command",
|
|
115
|
+
`Expand-Archive -LiteralPath '${filePath.replace(/'/g, "''")}' -DestinationPath '${outputDir.replace(/'/g, "''")}' -Force`,
|
|
116
|
+
],
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
command: "pwsh",
|
|
120
|
+
args: [
|
|
121
|
+
"-NoLogo",
|
|
122
|
+
"-NoProfile",
|
|
123
|
+
"-NonInteractive",
|
|
124
|
+
"-Command",
|
|
125
|
+
`Expand-Archive -LiteralPath '${filePath.replace(/'/g, "''")}' -DestinationPath '${outputDir.replace(/'/g, "''")}' -Force`,
|
|
126
|
+
],
|
|
127
|
+
},
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
for (const candidate of powershellCommands) {
|
|
131
|
+
const result = spawnSync(candidate.command, candidate.args, { stdio: "inherit" });
|
|
132
|
+
if (!result.error && result.status === 0) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (result.error && result.error.code !== "ENOENT") {
|
|
136
|
+
throw new Error(`使用 ${candidate.command} 解压 zip 资产失败:${filePath}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const result = spawnSync("tar", ["-xf", filePath, "-C", outputDir], { stdio: "inherit" });
|
|
142
|
+
if (result.status !== 0) {
|
|
143
|
+
if (process.platform === "win32" && result.error && result.error.code === "ENOENT") {
|
|
144
|
+
throw new Error(`当前 Windows 环境缺少 PowerShell Expand-Archive 和 tar,无法解压资产:${filePath}`);
|
|
145
|
+
}
|
|
146
|
+
throw new Error(`解压资产失败:${filePath}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function install() {
|
|
151
|
+
const target = detectTarget();
|
|
152
|
+
const binaryPath = vendorBinaryPath(target);
|
|
153
|
+
if (existsSync(binaryPath)) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const pkg = packageMetadata();
|
|
158
|
+
validatePackageVersion(pkg.version);
|
|
159
|
+
const packageKey = pkg.myte.packageKey;
|
|
160
|
+
const releaseRepository = pkg.myte.releaseRepository;
|
|
161
|
+
const releaseTag = `${packageKey}-v${pkg.version}`;
|
|
162
|
+
const assetName = `${releaseTag}-${target.key}.${target.assetExt}`;
|
|
163
|
+
const checksumName = `${releaseTag}-checksums.txt`;
|
|
164
|
+
const baseUrl = `https://github.com/${releaseRepository}/releases/download/${releaseTag}`;
|
|
165
|
+
const tempDir = mkdtempSync(join(tmpdir(), "myte-install-"));
|
|
166
|
+
const assetPath = join(tempDir, assetName);
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
const checksums = await fetchBuffer(`${baseUrl}/${checksumName}`);
|
|
170
|
+
await downloadFile(`${baseUrl}/${assetName}`, assetPath);
|
|
171
|
+
verifyChecksum(checksums.toString("utf8"), assetName, assetPath);
|
|
172
|
+
|
|
173
|
+
const extractDir = join(tempDir, "extract");
|
|
174
|
+
extractArchive(assetPath, extractDir);
|
|
175
|
+
|
|
176
|
+
const extractedBinary = join(extractDir, "package", "vendor", target.key, target.binary);
|
|
177
|
+
if (!existsSync(extractedBinary)) {
|
|
178
|
+
throw new Error(`资产中缺少平台二进制:${assetName}`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
mkdirSync(dirname(binaryPath), { recursive: true });
|
|
182
|
+
require("node:fs").copyFileSync(extractedBinary, binaryPath);
|
|
183
|
+
} finally {
|
|
184
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
install().catch((error) => {
|
|
189
|
+
console.error(`[myte] 安装失败:${error.message}`);
|
|
190
|
+
process.exit(1);
|
|
191
|
+
});
|