@heyinkeji/cli 0.2.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 +45 -0
- package/dist/cli.js +17 -0
- package/dist/download.js +44 -0
- package/dist/install.js +16 -0
- package/dist/platform.js +17 -0
- package/package.json +43 -0
- package/scripts/postinstall.mjs +6 -0
package/README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# @heyinkeji/cli
|
|
2
|
+
|
|
3
|
+
合因 Go CLI 的 npm 安装器。发布到 npmjs 后,用户可执行:
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npm install -g @heyinkeji/cli
|
|
7
|
+
heyincli version
|
|
8
|
+
heyincli --help
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
需要 Node.js 22 或更高版本。支持 Windows、macOS、Linux 的 amd64(Node.js 中叫 x64)和 arm64。Windows 的 npm 命令入口为 `heyincli.cmd`,在普通终端中可直接执行 `heyincli`。
|
|
12
|
+
|
|
13
|
+
安装时通过 HTTPS 下载与 npm 包版本相同的 Go 二进制,例如:
|
|
14
|
+
|
|
15
|
+
```text
|
|
16
|
+
https://heyin-public.oss-cn-beijing.aliyuncs.com/heyin-golang-cli/v0.2.0/darwin-arm64/heyincli
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
下载域名在 `package.json` 的 `heyin.downloadBaseUrl` 配置。二进制保存在已安装包的 `vendor/` 中,Windows 保存为 `heyincli.exe`,macOS/Linux 设置执行权限。Node 入口透传参数、标准输入输出、环境变量、工作目录及退出码,不需要用户安装 Go。安装过程需要访问该 OSS 地址,失败会返回非零状态;禁用安装脚本会导致二进制未安装,需开启安装脚本后重新安装。
|
|
20
|
+
|
|
21
|
+
## 开发与验证
|
|
22
|
+
|
|
23
|
+
在 `installer/` 中运行:
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
npm ci
|
|
27
|
+
npm test
|
|
28
|
+
npm audit
|
|
29
|
+
npm pack
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
源码目录的依赖安装不会下载二进制;`npm test` 编译 TypeScript 并执行本地 HTTPS 服务与进程测试(测试证书通过 OpenSSL 生成)。`npm pack` 会检查 npm 版本与 `../cmd/root.go` 的 `Version` 一致,再编译。产物仅包含 JavaScript 安装器、命令入口、说明和包配置,不携带六个平台的二进制或开发依赖。
|
|
33
|
+
|
|
34
|
+
## 发布
|
|
35
|
+
|
|
36
|
+
1. 同步 `package.json`、`package-lock.json` 的 npm 版本和 `cmd/root.go` 中的 CLI 版本;例如修改 Go 版本后运行 `npm version 0.2.0 --no-git-tag-version`。
|
|
37
|
+
2. 在 GitLab **Settings → CI/CD → Variables** 配置 `NPM_TOKEN`,环境范围为 `npm-release`,启用 Masked/Hidden。生产令牌启用 Protected,并在受保护分支或标签运行流水线。
|
|
38
|
+
3. 测试、版本检查和六个平台构建通过后,手动运行 `publish_oss`。
|
|
39
|
+
4. OSS 发布成功后,`publish_npm` 自动执行 `npm ci`、`npm test` 和 `npm publish --access public`。发布钩子会校验版本并编译安装器。
|
|
40
|
+
|
|
41
|
+
使用具备 `@heyinkeji/cli` 发布权限的 npmjs granular access token;首次发布时需有在 `@heyinkeji` scope 创建包的权限。非交互发布需允许 Bypass 2FA,参考 [npm CI/CD 认证说明](https://docs.npmjs.com/using-private-packages-in-a-ci-cd-workflow/)。认证配置仅写入临时文件,退出时清理,不放入 npm 包。
|
|
42
|
+
|
|
43
|
+
npm 发布失败时重试同一流水线的 `publish_npm`,无需重新上传 OSS;新流水线会被已存在的 OSS 发布标记阻止。npm 不允许覆盖已发布的同名同版本包,若该版本已成功发布,应先核对 npmjs 状态。也可在本地登录 npmjs 后执行 `npm publish --access public`。
|
|
44
|
+
|
|
45
|
+
安装器采用与 [xparse-cli](https://www.npmjs.com/package/xparse-cli) 和 [@wecom/cli](https://www.npmjs.com/package/@wecom/cli) 类似的 Node 命令入口;平台产物由本项目已有的 OSS 分发。安装钩子与打包钩子遵循 [npm 生命周期](https://docs.npmjs.com/cli/v11/using-npm/scripts/)。
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { constants } from 'node:os';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { resolveTarget } from './platform.js';
|
|
6
|
+
try {
|
|
7
|
+
const target = resolveTarget(process.platform, process.arch);
|
|
8
|
+
const binary = fileURLToPath(new URL(`../vendor/${target.binaryName}`, import.meta.url));
|
|
9
|
+
const result = spawnSync(binary, process.argv.slice(2), { stdio: 'inherit', windowsHide: false });
|
|
10
|
+
if (result.error)
|
|
11
|
+
throw new Error(`无法启动 heyincli,请重新安装:npm install -g @heyinkeji/cli。${result.error.message}`);
|
|
12
|
+
process.exitCode = result.status ?? (result.signal ? 128 + constants.signals[result.signal] : 1);
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
16
|
+
process.exitCode = 1;
|
|
17
|
+
}
|
package/dist/download.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { get } from 'node:https';
|
|
2
|
+
import { createWriteStream } from 'node:fs';
|
|
3
|
+
import { chmod, mkdir, mkdtemp, rename, rm, stat } from 'node:fs/promises';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
import { pipeline } from 'node:stream/promises';
|
|
6
|
+
async function response(url, options, signal, redirects = 0) {
|
|
7
|
+
if (url.protocol !== 'https:' || url.username || url.password)
|
|
8
|
+
throw new Error('下载和重定向仅允许无凭据的 HTTPS 地址。');
|
|
9
|
+
const res = await new Promise((resolve, reject) => {
|
|
10
|
+
const req = (options.request ?? get)(url, { signal }, resolve);
|
|
11
|
+
req.on('error', reject);
|
|
12
|
+
});
|
|
13
|
+
if ([301, 302, 303, 307, 308].includes(res.statusCode ?? 0)) {
|
|
14
|
+
res.destroy();
|
|
15
|
+
if (!res.headers.location || redirects >= 5)
|
|
16
|
+
throw new Error('下载重定向无效或次数过多。');
|
|
17
|
+
return response(new URL(res.headers.location, url), options, signal, redirects + 1);
|
|
18
|
+
}
|
|
19
|
+
if (res.statusCode !== 200) {
|
|
20
|
+
res.destroy();
|
|
21
|
+
throw new Error(`下载失败:HTTP ${res.statusCode},请确认对应 CLI 版本已发布。`);
|
|
22
|
+
}
|
|
23
|
+
return res;
|
|
24
|
+
}
|
|
25
|
+
export async function downloadBinary(url, destination, options = {}) {
|
|
26
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
27
|
+
const temporary = await mkdtemp(join(dirname(destination), '.download-'));
|
|
28
|
+
const partial = join(temporary, 'heyincli');
|
|
29
|
+
const controller = new AbortController();
|
|
30
|
+
const timer = setTimeout(() => controller.abort(), options.timeoutMs ?? 120_000);
|
|
31
|
+
try {
|
|
32
|
+
const res = await response(url, options, controller.signal);
|
|
33
|
+
await pipeline(res, createWriteStream(partial, { flags: 'wx', mode: 0o700 }), { signal: controller.signal });
|
|
34
|
+
if ((await stat(partial)).size === 0)
|
|
35
|
+
throw new Error('下载内容为空。');
|
|
36
|
+
await chmod(partial, 0o755);
|
|
37
|
+
await rename(partial, destination);
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
controller.abort();
|
|
41
|
+
clearTimeout(timer);
|
|
42
|
+
await rm(temporary, { recursive: true, force: true });
|
|
43
|
+
}
|
|
44
|
+
}
|
package/dist/install.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { resolveTarget, downloadUrl } from './platform.js';
|
|
4
|
+
import { downloadBinary } from './download.js';
|
|
5
|
+
try {
|
|
6
|
+
const { version, heyin } = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8'));
|
|
7
|
+
const target = resolveTarget(process.platform, process.arch);
|
|
8
|
+
const url = downloadUrl(heyin.downloadBaseUrl, version, target);
|
|
9
|
+
const destination = fileURLToPath(new URL(`../vendor/${target.binaryName}`, import.meta.url));
|
|
10
|
+
console.error(`正在安装 heyincli ${version} (${target.directory})…`);
|
|
11
|
+
await downloadBinary(url, destination);
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
console.error(`heyincli 安装失败:${error instanceof Error ? error.message : String(error)}`);
|
|
15
|
+
process.exitCode = 1;
|
|
16
|
+
}
|
package/dist/platform.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function resolveTarget(platform, arch) {
|
|
2
|
+
const system = { win32: 'windows', darwin: 'darwin', linux: 'linux' }[platform];
|
|
3
|
+
const cpu = { x64: 'amd64', arm64: 'arm64' }[arch];
|
|
4
|
+
if (!system || !cpu)
|
|
5
|
+
throw new Error(`不支持的平台:${platform}-${arch},仅支持 Windows/macOS/Linux 的 x64、arm64。`);
|
|
6
|
+
return { directory: `${system}-${cpu}`, binaryName: platform === 'win32' ? 'heyincli.exe' : 'heyincli' };
|
|
7
|
+
}
|
|
8
|
+
export function downloadUrl(baseUrl, version, target) {
|
|
9
|
+
const url = new URL(baseUrl);
|
|
10
|
+
if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash) {
|
|
11
|
+
throw new Error('下载地址必须是无凭据、查询参数和片段的 HTTPS 地址。');
|
|
12
|
+
}
|
|
13
|
+
if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version))
|
|
14
|
+
throw new Error('CLI 版本格式无效。');
|
|
15
|
+
url.pathname = `${url.pathname.replace(/\/$/, '')}/heyin-golang-cli/v${version}/${target.directory}/heyincli`;
|
|
16
|
+
return url;
|
|
17
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@heyinkeji/cli",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "合因平台命令行工具 heyincli 的安装器",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"heyincli": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist/",
|
|
11
|
+
"README.md",
|
|
12
|
+
"scripts/postinstall.mjs"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=22"
|
|
16
|
+
},
|
|
17
|
+
"os": [
|
|
18
|
+
"darwin",
|
|
19
|
+
"linux",
|
|
20
|
+
"win32"
|
|
21
|
+
],
|
|
22
|
+
"cpu": [
|
|
23
|
+
"x64",
|
|
24
|
+
"arm64"
|
|
25
|
+
],
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public",
|
|
28
|
+
"registry": "https://registry.npmjs.org/"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsc",
|
|
32
|
+
"test": "npm run build && node --test test/*.test.mjs",
|
|
33
|
+
"postinstall": "node scripts/postinstall.mjs",
|
|
34
|
+
"prepack": "node scripts/check-release.mjs && npm run build"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "22.20.1",
|
|
38
|
+
"typescript": "7.0.2"
|
|
39
|
+
},
|
|
40
|
+
"heyin": {
|
|
41
|
+
"downloadBaseUrl": "https://heyin-public.oss-cn-beijing.aliyuncs.com"
|
|
42
|
+
}
|
|
43
|
+
}
|