@heyinkeji/cli 0.2.0 → 0.3.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 +11 -3
- package/dist/env.js +66 -0
- package/dist/install.js +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,6 +18,14 @@ https://heyin-public.oss-cn-beijing.aliyuncs.com/heyin-golang-cli/v0.2.0/darwin-
|
|
|
18
18
|
|
|
19
19
|
下载域名在 `package.json` 的 `heyin.downloadBaseUrl` 配置。二进制保存在已安装包的 `vendor/` 中,Windows 保存为 `heyincli.exe`,macOS/Linux 设置执行权限。Node 入口透传参数、标准输入输出、环境变量、工作目录及退出码,不需要用户安装 Go。安装过程需要访问该 OSS 地址,失败会返回非零状态;禁用安装脚本会导致二进制未安装,需开启安装脚本后重新安装。
|
|
20
20
|
|
|
21
|
+
## 安装时保存配置
|
|
22
|
+
|
|
23
|
+
安装器在下载成功后校验并保存安装进程中受支持的有效配置:`ENDPOINT`、`OAUTH_CLIENT_ID` 和 `SOURCE`,写入当前用户的 `~/.heyin/.env`。WorkBuddy 连接器 `cli.json` 的 `env` 仅传入 `SOURCE`,例如 `{"SOURCE":"WorkBuddy"}`;`init` 仅需 `npm install -g @heyinkeji/cli`。
|
|
24
|
+
|
|
25
|
+
未设置或空白的项保留原值;全部为空时不创建、改写文件。`ENDPOINT` 必须是 HTTP(S) 源地址,不能包含凭据、业务路径、查询或片段;`OAUTH_CLIENT_ID` 是公开客户端 ID,不含空白或控制字符;`SOURCE` 是 OAuth 展示来源,不含控制字符。保存时更新同名项,保留其他配置及注释。任何配置校验失败都不写文件,错误不回显值。安装器不保存 PATH、NPM_TOKEN、CLIENT_SECRET 等非配置环境变量。
|
|
26
|
+
|
|
27
|
+
macOS/Linux 文件权限为 0600,Windows 沿用目录 ACL。CLI 配置名和预制默认值统一位于 `cmd/env.go`;CLI 优先读取 `.env`,缺失时使用默认值,安装器不把默认值写入文件。
|
|
28
|
+
|
|
21
29
|
## 开发与验证
|
|
22
30
|
|
|
23
31
|
在 `installer/` 中运行:
|
|
@@ -29,17 +37,17 @@ npm audit
|
|
|
29
37
|
npm pack
|
|
30
38
|
```
|
|
31
39
|
|
|
32
|
-
源码目录的依赖安装不会下载二进制;`npm test` 编译 TypeScript 并执行本地 HTTPS 服务与进程测试(测试证书通过 OpenSSL 生成)。`npm pack` 会检查 npm 版本与 `../cmd/
|
|
40
|
+
源码目录的依赖安装不会下载二进制;`npm test` 编译 TypeScript 并执行本地 HTTPS 服务与进程测试(测试证书通过 OpenSSL 生成)。`npm pack` 会检查 npm 版本与 `../cmd/env.go` 的 `Version` 一致,再编译。产物仅包含 JavaScript 安装器、命令入口、说明和包配置,不携带六个平台的二进制或开发依赖。
|
|
33
41
|
|
|
34
42
|
## 发布
|
|
35
43
|
|
|
36
|
-
1. 同步 `package.json`、`package-lock.json` 的 npm 版本和 `cmd/
|
|
44
|
+
1. 同步 `package.json`、`package-lock.json` 的 npm 版本和 `cmd/env.go` 中的 CLI 版本;例如修改 Go 版本后运行 `npm version 0.2.0 --no-git-tag-version`。
|
|
37
45
|
2. 在 GitLab **Settings → CI/CD → Variables** 配置 `NPM_TOKEN`,环境范围为 `npm-release`,启用 Masked/Hidden。生产令牌启用 Protected,并在受保护分支或标签运行流水线。
|
|
38
46
|
3. 测试、版本检查和六个平台构建通过后,手动运行 `publish_oss`。
|
|
39
47
|
4. OSS 发布成功后,`publish_npm` 自动执行 `npm ci`、`npm test` 和 `npm publish --access public`。发布钩子会校验版本并编译安装器。
|
|
40
48
|
|
|
41
49
|
使用具备 `@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
50
|
|
|
43
|
-
npm 发布失败时重试同一流水线的 `publish_npm`,无需重新上传 OSS
|
|
51
|
+
npm 发布失败时重试同一流水线的 `publish_npm`,无需重新上传 OSS;新流水线检测到已存在的 OSS 发布标记后会成功结束并跳过发布。npm 不允许覆盖已发布的同名同版本包,若该版本已成功发布,应先核对 npmjs 状态。也可在本地登录 npmjs 后执行 `npm publish --access public`。
|
|
44
52
|
|
|
45
53
|
安装器采用与 [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/env.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { lstat, mkdir, mkdtemp, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
// 仅保存 CLI 支持的配置,不能将 PATH、NPM_TOKEN 等进程环境持久化。
|
|
5
|
+
export async function saveEnvironment(env = process.env, home = homedir()) {
|
|
6
|
+
const endpoint = env.ENDPOINT?.trim();
|
|
7
|
+
const source = env.SOURCE?.trim();
|
|
8
|
+
const clientID = env.OAUTH_CLIENT_ID?.trim();
|
|
9
|
+
if (endpoint)
|
|
10
|
+
validateEndpoint(endpoint);
|
|
11
|
+
if (clientID && /[\s\p{Cc}\p{Cf}]/u.test(clientID))
|
|
12
|
+
throw new Error('OAUTH_CLIENT_ID 不能包含空白或控制字符。');
|
|
13
|
+
if (source && /[\u0000-\u001f\u007f]/.test(source))
|
|
14
|
+
throw new Error('SOURCE 不能包含控制字符。');
|
|
15
|
+
const values = {
|
|
16
|
+
...(endpoint ? { ENDPOINT: endpoint } : {}),
|
|
17
|
+
...(source ? { SOURCE: source } : {}),
|
|
18
|
+
...(clientID ? { OAUTH_CLIENT_ID: clientID } : {}),
|
|
19
|
+
};
|
|
20
|
+
if (Object.keys(values).length === 0)
|
|
21
|
+
return;
|
|
22
|
+
const directory = join(home, '.heyin');
|
|
23
|
+
const file = join(directory, '.env');
|
|
24
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
25
|
+
if (!(await lstat(directory)).isDirectory())
|
|
26
|
+
throw new Error('配置目录必须为普通目录。');
|
|
27
|
+
let previous = '';
|
|
28
|
+
try {
|
|
29
|
+
if (!(await lstat(file)).isFile())
|
|
30
|
+
throw new Error('环境配置必须为普通文件。');
|
|
31
|
+
previous = await readFile(file, 'utf8');
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
if (error.code !== 'ENOENT')
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
const lines = previous.split(/\r?\n/).filter(line => {
|
|
38
|
+
const key = line.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/)?.[1];
|
|
39
|
+
return !key || !Object.hasOwn(values, key);
|
|
40
|
+
});
|
|
41
|
+
while (lines.length && lines.at(-1) === '')
|
|
42
|
+
lines.pop();
|
|
43
|
+
const updates = Object.entries(values).map(([key, value]) => `${key}=${JSON.stringify(value)}`);
|
|
44
|
+
const content = [...lines, ...updates, ''].join('\n');
|
|
45
|
+
const temporary = await mkdtemp(join(directory, '.env-'));
|
|
46
|
+
try {
|
|
47
|
+
const pending = join(temporary, '.env');
|
|
48
|
+
await writeFile(pending, content, { mode: 0o600, flag: 'wx' });
|
|
49
|
+
await rename(pending, file);
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
await rm(temporary, { recursive: true, force: true });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function validateEndpoint(value) {
|
|
56
|
+
try {
|
|
57
|
+
const url = new URL(value);
|
|
58
|
+
if (/[\u0000-\u0020\u007f]/.test(value) || !['https:', 'http:'].includes(url.protocol) ||
|
|
59
|
+
url.username || url.password || url.search || url.hash || url.pathname !== '/') {
|
|
60
|
+
throw new Error();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
throw new Error('ENDPOINT 必须为 HTTP(S) 源地址,不能包含凭据、业务路径、查询参数或片段。');
|
|
65
|
+
}
|
|
66
|
+
}
|
package/dist/install.js
CHANGED
|
@@ -2,6 +2,7 @@ import { readFile } from 'node:fs/promises';
|
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
3
|
import { resolveTarget, downloadUrl } from './platform.js';
|
|
4
4
|
import { downloadBinary } from './download.js';
|
|
5
|
+
import { saveEnvironment } from './env.js';
|
|
5
6
|
try {
|
|
6
7
|
const { version, heyin } = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8'));
|
|
7
8
|
const target = resolveTarget(process.platform, process.arch);
|
|
@@ -9,6 +10,7 @@ try {
|
|
|
9
10
|
const destination = fileURLToPath(new URL(`../vendor/${target.binaryName}`, import.meta.url));
|
|
10
11
|
console.error(`正在安装 heyincli ${version} (${target.directory})…`);
|
|
11
12
|
await downloadBinary(url, destination);
|
|
13
|
+
await saveEnvironment();
|
|
12
14
|
}
|
|
13
15
|
catch (error) {
|
|
14
16
|
console.error(`heyincli 安装失败:${error instanceof Error ? error.message : String(error)}`);
|