@windaka-erp/erp-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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windaka-erp/erp-cli",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "ERP CLI - command-line tool for Windaka ERP system",
5
5
  "bin": {
6
6
  "erp-cli": "scripts/run.js"
@@ -36,5 +36,8 @@
36
36
  "cpu": [
37
37
  "x64",
38
38
  "arm64"
39
- ]
39
+ ],
40
+ "dependencies": {
41
+ "@windaka-erp/erp-cli": "^0.2.0"
42
+ }
40
43
  }
@@ -6,6 +6,7 @@ const fs = require("fs");
6
6
  const BINARY_NAME = "erp-cli";
7
7
  const VERSION = require("../package.json").version;
8
8
  const GITHUB_REPO = "windaka-erp/erp-cli";
9
+ const GITEE_REPO = "xing-wenkai/erp-cli";
9
10
 
10
11
  // ── 平台映射 ──
11
12
  function getPlatformInfo() {
@@ -30,10 +31,56 @@ function getPlatformInfo() {
30
31
  return info;
31
32
  }
32
33
 
33
- // ── 下载地址 ──
34
- function getDownloadURL(info) {
34
+ // ── 下载地址(GitHub 优先,Gitee 回退)──
35
+ function getDownloadURLs(info) {
35
36
  const filename = `${BINARY_NAME}-${info.target}${info.ext}`;
36
- return `https://github.com/${GITHUB_REPO}/releases/download/v${VERSION}/${filename}`;
37
+ return [
38
+ `https://github.com/${GITHUB_REPO}/releases/download/v${VERSION}/${filename}`,
39
+ `https://gitee.com/${GITEE_REPO}/releases/download/v${VERSION}/${filename}`,
40
+ ];
41
+ }
42
+
43
+ // ── 获取已安装二进制的版本号 ──
44
+ function getInstalledVersion(binaryPath) {
45
+ try {
46
+ const output = execSync(`"${binaryPath}" --version`, {
47
+ encoding: "utf8",
48
+ timeout: 5000,
49
+ });
50
+ // 输出格式: "erp-cli version v0.2.0" 或 "erp-cli version 0.2.0"
51
+ const match = output.match(/version\s+v?(\S+)/i);
52
+ return match ? match[1] : null;
53
+ } catch {
54
+ return null;
55
+ }
56
+ }
57
+
58
+ // ── 比较版本号 ──
59
+ function isVersionOutdated(installed, expected) {
60
+ if (!installed) return true;
61
+ // 去掉 v 前缀统一比较
62
+ const a = installed.replace(/^v/, "");
63
+ const b = expected.replace(/^v/, "");
64
+ return a !== b;
65
+ }
66
+
67
+ // ── 下载二进制 ──
68
+ function downloadBinary(url, binaryPath) {
69
+ if (process.platform === "win32") {
70
+ execSync(
71
+ `powershell -Command "Invoke-WebRequest -Uri '${url}' -OutFile '${binaryPath}'"`,
72
+ { stdio: "inherit" }
73
+ );
74
+ } else {
75
+ execSync(`curl -L -o "${binaryPath}" "${url}"`, {
76
+ stdio: "inherit",
77
+ });
78
+ }
79
+
80
+ // 设置可执行权限(非 Windows)
81
+ if (process.platform !== "win32") {
82
+ fs.chmodSync(binaryPath, 0o755);
83
+ }
37
84
  }
38
85
 
39
86
  // ── 主流程 ──
@@ -47,10 +94,17 @@ function install() {
47
94
  const info = getPlatformInfo();
48
95
  const binaryPath = path.join(binDir, `${BINARY_NAME}${info.ext}`);
49
96
 
50
- // 1. 检查 bin/ 目录是否已有对应平台的二进制(npm pack 打包时带进来的)
97
+ // 1. 检查 bin/ 目录是否已有对应平台的二进制
51
98
  if (fs.existsSync(binaryPath)) {
52
- console.log(`Found bundled binary: ${binaryPath}`);
53
- return;
99
+ const installedVer = getInstalledVersion(binaryPath);
100
+ if (!isVersionOutdated(installedVer, VERSION)) {
101
+ console.log(`Found bundled binary: ${binaryPath} (v${installedVer})`);
102
+ return;
103
+ }
104
+ // 版本不一致,需要更新
105
+ console.log(
106
+ `Binary version mismatch: installed v${installedVer}, expected v${VERSION}. Updating...`
107
+ );
54
108
  }
55
109
 
56
110
  // 2. 检查项目根目录是否有本地编译产物(开发模式)
@@ -62,33 +116,25 @@ function install() {
62
116
  return;
63
117
  }
64
118
 
65
- // 3. 从 GitHub Releases 下载
66
- const url = getDownloadURL(info);
119
+ // 3. 从 GitHub Releases 下载,失败回退 Gitee
120
+ const urls = getDownloadURLs(info);
67
121
  console.log(`Downloading ${BINARY_NAME} v${VERSION} for ${info.target}...`);
68
- console.log(` URL: ${url}`);
69
122
 
70
- try {
71
- if (process.platform === "win32") {
72
- // Windows: 使用 PowerShell 下载
73
- execSync(
74
- `powershell -Command "Invoke-WebRequest -Uri '${url}' -OutFile '${binaryPath}'"`,
75
- { stdio: "inherit" }
76
- );
77
- } else {
78
- // macOS/Linux: 使用 curl
79
- execSync(`curl -L -o "${binaryPath}" "${url}"`, {
80
- stdio: "inherit",
81
- });
82
- }
83
-
84
- // 设置可执行权限(非 Windows)
85
- if (process.platform !== "win32") {
86
- fs.chmodSync(binaryPath, 0o755);
123
+ let downloaded = false;
124
+ for (const url of urls) {
125
+ console.log(` Trying: ${url}`);
126
+ try {
127
+ downloadBinary(url, binaryPath);
128
+ downloaded = true;
129
+ console.log(`Successfully installed ${BINARY_NAME} v${VERSION}`);
130
+ break;
131
+ } catch (err) {
132
+ console.warn(` Failed: ${err.message}`);
87
133
  }
134
+ }
88
135
 
89
- console.log(`Successfully installed ${BINARY_NAME} v${VERSION}`);
90
- } catch (err) {
91
- console.error(`Download failed: ${err.message}`);
136
+ if (!downloaded) {
137
+ console.error(`All download sources failed.`);
92
138
  console.error(`You can build from source:`);
93
139
  console.error(` git clone https://github.com/${GITHUB_REPO}.git`);
94
140
  console.error(` cd erp-cli && go build -o ${BINARY_NAME} .`);
@@ -97,4 +143,4 @@ function install() {
97
143
  }
98
144
  }
99
145
 
100
- install();
146
+ install();
package/scripts/run.js CHANGED
@@ -1,14 +1,18 @@
1
1
  #!/usr/bin/env node
2
- const { execFileSync } = require("child_process");
2
+ const { execFileSync, execSync } = require("child_process");
3
3
  const path = require("path");
4
4
  const fs = require("fs");
5
+ const https = require("https");
6
+ const http = require("http");
5
7
 
6
8
  const BINARY_NAME = "erp-cli";
9
+ const VERSION = require("../package.json").version;
10
+ const GITHUB_REPO = "windaka-erp/erp-cli";
11
+ const GITEE_REPO = "xing-wenkai/erp-cli";
7
12
 
8
13
  function getBinaryPath() {
9
14
  const binDir = path.join(__dirname, "..", "bin");
10
15
 
11
- // 按优先级查找二进制
12
16
  const candidates = [
13
17
  path.join(binDir, `${BINARY_NAME}.exe`), // bin/erp-cli.exe (Windows)
14
18
  path.join(binDir, BINARY_NAME), // bin/erp-cli (macOS/Linux)
@@ -25,7 +29,182 @@ function getBinaryPath() {
25
29
  process.exit(1);
26
30
  }
27
31
 
32
+ // ── 静默自动更新 ──
33
+
34
+ function getPlatformInfo() {
35
+ const platform = process.platform;
36
+ const arch = process.arch;
37
+
38
+ const map = {
39
+ win32_x64: { target: "windows-amd64", ext: ".exe" },
40
+ win32_arm64: { target: "windows-arm64", ext: ".exe" },
41
+ darwin_x64: { target: "darwin-amd64", ext: "" },
42
+ darwin_arm64: { target: "darwin-arm64", ext: "" },
43
+ linux_x64: { target: "linux-amd64", ext: "" },
44
+ linux_arm64: { target: "linux-arm64", ext: "" },
45
+ };
46
+
47
+ return map[`${platform}_${arch}`] || null;
48
+ }
49
+
50
+ function getInstalledVersion(binaryPath) {
51
+ try {
52
+ const output = execSync(`"${binaryPath}" --version`, {
53
+ encoding: "utf8",
54
+ timeout: 5000,
55
+ });
56
+ const match = output.match(/version\s+v?(\S+)/i);
57
+ return match ? match[1].replace(/^v/, "") : null;
58
+ } catch {
59
+ return null;
60
+ }
61
+ }
62
+
63
+ function fetchJSON(url) {
64
+ return new Promise((resolve, reject) => {
65
+ const client = url.startsWith("https") ? https : http;
66
+ client
67
+ .get(url, { timeout: 5000 }, (res) => {
68
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
69
+ fetchJSON(res.headers.location).then(resolve, reject);
70
+ return;
71
+ }
72
+ if (res.statusCode !== 200) {
73
+ reject(new Error(`HTTP ${res.statusCode}`));
74
+ return;
75
+ }
76
+ let data = "";
77
+ res.on("data", (chunk) => (data += chunk));
78
+ res.on("end", () => {
79
+ try {
80
+ resolve(JSON.parse(data));
81
+ } catch (e) {
82
+ reject(e);
83
+ }
84
+ });
85
+ })
86
+ .on("error", reject)
87
+ .on("timeout", function () {
88
+ this.destroy();
89
+ reject(new Error("timeout"));
90
+ });
91
+ });
92
+ }
93
+
94
+ function downloadFile(url, dest) {
95
+ return new Promise((resolve, reject) => {
96
+ const client = url.startsWith("https") ? https : http;
97
+ client
98
+ .get(url, (res) => {
99
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
100
+ downloadFile(res.headers.location, dest).then(resolve, reject);
101
+ return;
102
+ }
103
+ if (res.statusCode !== 200) {
104
+ reject(new Error(`HTTP ${res.statusCode}`));
105
+ return;
106
+ }
107
+ const stream = fs.createWriteStream(dest);
108
+ res.pipe(stream);
109
+ stream.on("finish", () => {
110
+ stream.close();
111
+ if (process.platform !== "win32") {
112
+ fs.chmodSync(dest, 0o755);
113
+ }
114
+ resolve();
115
+ });
116
+ stream.on("error", reject);
117
+ })
118
+ .on("error", reject);
119
+ });
120
+ }
121
+
122
+ async function silentAutoUpdate() {
123
+ try {
124
+ const binary = getBinaryPath();
125
+ const installedVer = getInstalledVersion(binary);
126
+
127
+ // 如果已安装版本和 package.json 一致,跳过
128
+ if (installedVer === VERSION.replace(/^v/, "")) return;
129
+
130
+ // 已安装版本比 package.json 新?跳过
131
+ if (installedVer && installedVer > VERSION.replace(/^v/, "")) return;
132
+
133
+ // 检查上次更新时间,每天最多检查一次
134
+ const updateMarker = path.join(
135
+ require("os").homedir(),
136
+ ".erp-cli",
137
+ ".last-update-check"
138
+ );
139
+ try {
140
+ const lastCheck = parseInt(fs.readFileSync(updateMarker, "utf8"), 10);
141
+ const ONE_DAY = 24 * 60 * 60 * 1000;
142
+ if (Date.now() - lastCheck < ONE_DAY) return;
143
+ } catch {
144
+ // 文件不存在或读取失败,继续检查
145
+ }
146
+ if (installedVer && installedVer > VERSION.replace(/^v/, "")) return;
147
+
148
+ // 需要更新:查询 Release 获取最新版本号(GitHub 优先,Gitee 回退)
149
+ let release;
150
+ try {
151
+ release = await fetchJSON(
152
+ `https://api.github.com/repos/${GITHUB_REPO}/releases/latest`
153
+ );
154
+ } catch {
155
+ // GitHub 不通,回退 Gitee API
156
+ release = await fetchJSON(
157
+ `https://gitee.com/api/v5/repos/${GITEE_REPO}/releases/latest`
158
+ );
159
+ }
160
+
161
+ const latestTag = release.tag_name; // e.g. "v0.3.0"
162
+ const latestVer = latestTag.replace(/^v/, "");
163
+
164
+ // 最新版和 package.json 版本不一致?等 npm update 后再说
165
+ if (latestVer !== VERSION.replace(/^v/, "")) return;
166
+
167
+ // 下载新版本
168
+ const info = getPlatformInfo();
169
+ if (!info) return;
170
+
171
+ const filename = `${BINARY_NAME}-${info.target}${info.ext}`;
172
+ const asset = release.assets.find((a) => a.name === filename);
173
+ if (!asset) return;
174
+
175
+ const binDir = path.join(__dirname, "..", "bin");
176
+ const binaryPath = path.join(binDir, `${BINARY_NAME}${info.ext}`);
177
+
178
+ // 下载到临时文件,完成后原子替换
179
+ const tmpPath = binaryPath + ".tmp";
180
+ await downloadFile(asset.browser_download_url, tmpPath);
181
+
182
+ // 替换旧二进制
183
+ fs.renameSync(tmpPath, binaryPath);
184
+
185
+ // 写入本次检查时间
186
+ try {
187
+ const markerDir = path.join(require("os").homedir(), ".erp-cli");
188
+ if (!fs.existsSync(markerDir)) fs.mkdirSync(markerDir, { recursive: true });
189
+ fs.writeFileSync(markerDir + "/.last-update-check", String(Date.now()));
190
+ } catch {
191
+ // 写入失败不影响
192
+ }
193
+ } catch {
194
+ // 静默失败,不影响正常使用
195
+ // 即使失败也写入检查时间,避免频繁重试
196
+ try {
197
+ const markerDir = path.join(require("os").homedir(), ".erp-cli");
198
+ if (!fs.existsSync(markerDir)) fs.mkdirSync(markerDir, { recursive: true });
199
+ fs.writeFileSync(markerDir + "/.last-update-check", String(Date.now()));
200
+ } catch {}
201
+ }
202
+ }
203
+
28
204
  function main() {
205
+ // 后台静默更新,不阻塞主流程
206
+ silentAutoUpdate().catch(() => {});
207
+
29
208
  const binary = getBinaryPath();
30
209
  const args = process.argv.slice(2);
31
210
 
@@ -35,10 +214,8 @@ function main() {
35
214
  env: { ...process.env },
36
215
  });
37
216
  } catch (err) {
38
- // execFileSync throws on non-zero exit codes; the binary already
39
- // wrote its output to stdout/stderr, so just exit with same code.
40
217
  process.exitCode = err.status || 1;
41
218
  }
42
219
  }
43
220
 
44
- main();
221
+ main();