@smallfishworld/open-code-review-svn 1.11.2-svn.1
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 +201 -0
- package/README.ja-JP.md +231 -0
- package/README.ko-KR.md +231 -0
- package/README.md +231 -0
- package/README.ru-RU.md +231 -0
- package/README.zh-CN.md +231 -0
- package/bin/ocr.js +85 -0
- package/imgs/benchmark-en.png +0 -0
- package/imgs/benchmark-ja.png +0 -0
- package/imgs/benchmark-ru.png +0 -0
- package/imgs/benchmark-zh.png +0 -0
- package/imgs/highlights-en.png +0 -0
- package/imgs/highlights-ja.png +0 -0
- package/imgs/highlights-ru.png +0 -0
- package/imgs/highlights-zh.png +0 -0
- package/imgs/logo-core.svg +1 -0
- package/imgs/logo.svg +1 -0
- package/imgs/providers.jpg +0 -0
- package/package.json +45 -0
- package/scripts/install.js +272 -0
- package/scripts/platform.js +69 -0
- package/scripts/update.js +170 -0
- package/scripts/version.js +45 -0
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@smallfishworld/open-code-review-svn",
|
|
3
|
+
"version": "1.11.2-svn.1",
|
|
4
|
+
"description": "OpenCodeReview CLI fork with SVN workspace review support",
|
|
5
|
+
"bin": {
|
|
6
|
+
"ocr": "bin/ocr.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"bin/ocr.js",
|
|
10
|
+
"scripts/install.js",
|
|
11
|
+
"scripts/update.js",
|
|
12
|
+
"scripts/version.js",
|
|
13
|
+
"scripts/platform.js",
|
|
14
|
+
"imgs/"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"postinstall": "node scripts/install.js",
|
|
18
|
+
"test:github-actions": "node scripts/github-actions/post-review-comments.test.js && node scripts/github-actions/check-translation-sync.test.js && node scripts/github-actions/action-contract.test.js",
|
|
19
|
+
"test:update": "node scripts/version.test.js",
|
|
20
|
+
"test:launcher": "node bin/ocr.test.js"
|
|
21
|
+
},
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/smallfishworld/open-code-review-svn.git"
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"ocrConfig": {
|
|
30
|
+
"urlPattern": "https://github.com/smallfishworld/open-code-review-svn/releases/download/v{version}/opencodereview-{os}-{arch}",
|
|
31
|
+
"checksumPattern": "https://github.com/smallfishworld/open-code-review-svn/releases/download/v{version}/sha256sum.txt"
|
|
32
|
+
},
|
|
33
|
+
"optionalDependencies": {
|
|
34
|
+
"@smallfishworld/ocr-darwin-arm64": "1.11.2-svn.1",
|
|
35
|
+
"@smallfishworld/ocr-darwin-x64": "1.11.2-svn.1",
|
|
36
|
+
"@smallfishworld/ocr-linux-arm64": "1.11.2-svn.1",
|
|
37
|
+
"@smallfishworld/ocr-linux-x64": "1.11.2-svn.1",
|
|
38
|
+
"@smallfishworld/ocr-win32-arm64": "1.11.2-svn.1",
|
|
39
|
+
"@smallfishworld/ocr-win32-x64": "1.11.2-svn.1"
|
|
40
|
+
},
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=14"
|
|
43
|
+
},
|
|
44
|
+
"license": "Apache-2.0"
|
|
45
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
// Copyright 2026 alibaba/open-code-review Contributors
|
|
5
|
+
|
|
6
|
+
"use strict";
|
|
7
|
+
|
|
8
|
+
const fs = require("fs");
|
|
9
|
+
const path = require("path");
|
|
10
|
+
const https = require("https");
|
|
11
|
+
const crypto = require("crypto");
|
|
12
|
+
|
|
13
|
+
const { IS_WINDOWS, BINARY_FILENAME: BINARY_NAME, resolveNativeBinary } = require("./platform");
|
|
14
|
+
|
|
15
|
+
const packageRoot = path.join(__dirname, "..");
|
|
16
|
+
const binDir = path.join(packageRoot, "bin");
|
|
17
|
+
const binaryDest = path.join(binDir, BINARY_NAME);
|
|
18
|
+
|
|
19
|
+
function info(msg) {
|
|
20
|
+
console.log(`[INFO] ${msg}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function warn(msg) {
|
|
24
|
+
console.warn(`[WARN] ${msg}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function error(msg) {
|
|
28
|
+
console.error(`[ERROR] ${msg}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function detectPlatform() {
|
|
32
|
+
let os = process.platform;
|
|
33
|
+
let arch = process.arch;
|
|
34
|
+
|
|
35
|
+
switch (arch) {
|
|
36
|
+
case "x64":
|
|
37
|
+
arch = "amd64";
|
|
38
|
+
break;
|
|
39
|
+
case "arm64":
|
|
40
|
+
arch = "arm64";
|
|
41
|
+
break;
|
|
42
|
+
default:
|
|
43
|
+
throw new Error(
|
|
44
|
+
`Unsupported architecture: ${arch}. Supported: amd64 (x64), arm64`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
switch (os) {
|
|
49
|
+
case "linux":
|
|
50
|
+
case "darwin":
|
|
51
|
+
break;
|
|
52
|
+
case "win32":
|
|
53
|
+
os = "windows";
|
|
54
|
+
break;
|
|
55
|
+
default:
|
|
56
|
+
throw new Error(
|
|
57
|
+
`Unsupported operating system: ${os}. Supported: linux, darwin, win32`
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return { os, arch };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function loadPackageJson() {
|
|
65
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8"));
|
|
66
|
+
if (!pkg.version) {
|
|
67
|
+
throw new Error("Missing version field in package.json");
|
|
68
|
+
}
|
|
69
|
+
if (!pkg.ocrConfig || !pkg.ocrConfig.urlPattern) {
|
|
70
|
+
throw new Error("Missing ocrConfig.urlPattern in package.json");
|
|
71
|
+
}
|
|
72
|
+
return pkg;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function resolveVersion(pkg) {
|
|
76
|
+
const envVersion = process.env.OCR_VERSION;
|
|
77
|
+
if (envVersion) {
|
|
78
|
+
const v = envVersion.startsWith("v") ? envVersion.slice(1) : envVersion;
|
|
79
|
+
info(`Using pinned version from OCR_VERSION: ${v}`);
|
|
80
|
+
return v;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
info(`Using version from package.json: ${pkg.version}`);
|
|
84
|
+
return pkg.version;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function buildUrl(pattern, vars) {
|
|
88
|
+
return pattern
|
|
89
|
+
.replace(/\{version\}/g, vars.version)
|
|
90
|
+
.replace(/\{os\}/g, vars.os)
|
|
91
|
+
.replace(/\{arch\}/g, vars.arch);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function download(url, maxRedirects = 10) {
|
|
95
|
+
if (!url.startsWith("https")) {
|
|
96
|
+
return Promise.reject(new Error(`Refusing non-HTTPS download: ${url}`));
|
|
97
|
+
}
|
|
98
|
+
if (maxRedirects <= 0) {
|
|
99
|
+
return Promise.reject(new Error(`Too many redirects fetching ${url}`));
|
|
100
|
+
}
|
|
101
|
+
return new Promise((resolve, reject) => {
|
|
102
|
+
https.get(url, (res) => {
|
|
103
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
104
|
+
res.resume();
|
|
105
|
+
download(res.headers.location, maxRedirects - 1).then(resolve).catch(reject);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (res.statusCode !== 200) {
|
|
109
|
+
res.resume();
|
|
110
|
+
reject(new Error(`HTTP ${res.statusCode} fetching ${url}`));
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
resolve(res);
|
|
114
|
+
}).on("error", reject);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function downloadText(url) {
|
|
119
|
+
const res = await download(url);
|
|
120
|
+
return new Promise((resolve, reject) => {
|
|
121
|
+
let data = "";
|
|
122
|
+
res.on("data", (chunk) => (data += chunk));
|
|
123
|
+
res.on("end", () => resolve(data));
|
|
124
|
+
res.on("error", reject);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function downloadBinary(url, destPath) {
|
|
129
|
+
const res = await download(url);
|
|
130
|
+
return new Promise((resolve, reject) => {
|
|
131
|
+
const fileStream = fs.createWriteStream(destPath);
|
|
132
|
+
res.on("error", (err) => {
|
|
133
|
+
fileStream.destroy();
|
|
134
|
+
fs.unlink(destPath, () => {});
|
|
135
|
+
reject(err);
|
|
136
|
+
});
|
|
137
|
+
res.pipe(fileStream);
|
|
138
|
+
fileStream.on("finish", () => fileStream.close(() => resolve()));
|
|
139
|
+
fileStream.on("error", (err) => {
|
|
140
|
+
fs.unlink(destPath, () => {});
|
|
141
|
+
reject(err);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function computeChecksum(filePath) {
|
|
147
|
+
return new Promise((resolve, reject) => {
|
|
148
|
+
const hash = crypto.createHash("sha256");
|
|
149
|
+
const stream = fs.createReadStream(filePath);
|
|
150
|
+
stream.on("data", (chunk) => hash.update(chunk));
|
|
151
|
+
stream.on("end", () => resolve(hash.digest("hex")));
|
|
152
|
+
stream.on("error", reject);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function main() {
|
|
157
|
+
info("OpenCodeReview Installer");
|
|
158
|
+
info("=========================");
|
|
159
|
+
|
|
160
|
+
const existing = resolveNativeBinary();
|
|
161
|
+
if (existing && existing.fromPlatformPkg) {
|
|
162
|
+
info("Binary provided by platform package, skipping download.");
|
|
163
|
+
info(` ${existing.path}`);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const { os, arch } = detectPlatform();
|
|
168
|
+
info(`Detected platform: ${os}/${arch}`);
|
|
169
|
+
|
|
170
|
+
const pkg = loadPackageJson();
|
|
171
|
+
const version = resolveVersion(pkg);
|
|
172
|
+
const config = pkg.ocrConfig;
|
|
173
|
+
|
|
174
|
+
if (!fs.existsSync(binDir)) {
|
|
175
|
+
fs.mkdirSync(binDir, { recursive: true });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (!IS_WINDOWS) {
|
|
179
|
+
const jsWrapper = path.join(binDir, "ocr.js");
|
|
180
|
+
if (fs.existsSync(jsWrapper)) {
|
|
181
|
+
try {
|
|
182
|
+
fs.chmodSync(jsWrapper, 0o755);
|
|
183
|
+
} catch (e) {
|
|
184
|
+
warn(`Could not make ocr.js executable: ${e.message}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const vars = { version, os, arch };
|
|
190
|
+
let downloadUrl = buildUrl(config.urlPattern, vars);
|
|
191
|
+
if (IS_WINDOWS) {
|
|
192
|
+
downloadUrl += ".exe";
|
|
193
|
+
}
|
|
194
|
+
info(`Downloading ${downloadUrl} ...`);
|
|
195
|
+
|
|
196
|
+
await downloadBinary(downloadUrl, binaryDest);
|
|
197
|
+
if (!IS_WINDOWS) {
|
|
198
|
+
fs.chmodSync(binaryDest, 0o755);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (config.checksumPattern) {
|
|
202
|
+
const checksumUrl = buildUrl(config.checksumPattern, vars);
|
|
203
|
+
info("Verifying checksum...");
|
|
204
|
+
let shaContent;
|
|
205
|
+
try {
|
|
206
|
+
shaContent = await downloadText(checksumUrl);
|
|
207
|
+
} catch (e) {
|
|
208
|
+
try { fs.unlinkSync(binaryDest); } catch (_) {}
|
|
209
|
+
throw new Error(`Failed to download checksum from ${checksumUrl}: ${e.message}`);
|
|
210
|
+
}
|
|
211
|
+
let actualSha;
|
|
212
|
+
try {
|
|
213
|
+
actualSha = await computeChecksum(binaryDest);
|
|
214
|
+
} catch (e) {
|
|
215
|
+
try { fs.unlinkSync(binaryDest); } catch (_) {}
|
|
216
|
+
throw new Error(`Failed to compute checksum for ${binaryDest}: ${e.message}`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
let verified = false;
|
|
220
|
+
for (const line of shaContent.split("\n")) {
|
|
221
|
+
const trimmed = line.trim();
|
|
222
|
+
if (trimmed.includes(`-${os}-${arch}`)) {
|
|
223
|
+
const expectedSha = trimmed.split(/\s+/)[0].toLowerCase();
|
|
224
|
+
if (expectedSha) {
|
|
225
|
+
if (actualSha !== expectedSha) {
|
|
226
|
+
try { fs.unlinkSync(binaryDest); } catch (_) {}
|
|
227
|
+
throw new Error(
|
|
228
|
+
`Checksum mismatch! Expected: ${expectedSha}, Got: ${actualSha}`
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
info("Checksum verified.");
|
|
232
|
+
verified = true;
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (!verified) {
|
|
238
|
+
try { fs.unlinkSync(binaryDest); } catch (_) {}
|
|
239
|
+
throw new Error(
|
|
240
|
+
`No matching checksum entry for ${os}-${arch} in ${checksumUrl}`
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
info(`Installed: ${binaryDest}`);
|
|
246
|
+
info("");
|
|
247
|
+
info("OpenCodeReview is ready!");
|
|
248
|
+
info("");
|
|
249
|
+
info("Quick start:");
|
|
250
|
+
info(" ocr version Show version info");
|
|
251
|
+
info(" ocr config set Configure your LLM provider");
|
|
252
|
+
info(" ocr review Start a code review");
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (require.main === module) {
|
|
256
|
+
main().catch((err) => {
|
|
257
|
+
error(err.message);
|
|
258
|
+
process.exit(1);
|
|
259
|
+
});
|
|
260
|
+
} else {
|
|
261
|
+
module.exports = {
|
|
262
|
+
IS_WINDOWS,
|
|
263
|
+
BINARY_NAME,
|
|
264
|
+
detectPlatform,
|
|
265
|
+
loadPackageJson,
|
|
266
|
+
buildUrl,
|
|
267
|
+
download,
|
|
268
|
+
downloadText,
|
|
269
|
+
downloadBinary,
|
|
270
|
+
computeChecksum,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright 2026 alibaba/open-code-review Contributors
|
|
3
|
+
|
|
4
|
+
"use strict";
|
|
5
|
+
|
|
6
|
+
const path = require("path");
|
|
7
|
+
const fs = require("fs");
|
|
8
|
+
|
|
9
|
+
const IS_WINDOWS = process.platform === "win32";
|
|
10
|
+
const BINARY_FILENAME = IS_WINDOWS ? "opencodereview.exe" : "opencodereview";
|
|
11
|
+
|
|
12
|
+
const PLATFORM_PKG = {
|
|
13
|
+
"darwin-arm64": "@smallfishworld/ocr-darwin-arm64",
|
|
14
|
+
"darwin-x64": "@smallfishworld/ocr-darwin-x64",
|
|
15
|
+
"linux-arm64": "@smallfishworld/ocr-linux-arm64",
|
|
16
|
+
"linux-x64": "@smallfishworld/ocr-linux-x64",
|
|
17
|
+
"win32-arm64": "@smallfishworld/ocr-win32-arm64",
|
|
18
|
+
"win32-x64": "@smallfishworld/ocr-win32-x64",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function getPlatformPackageName() {
|
|
22
|
+
const key = `${process.platform}-${process.arch}`;
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const parentPkg = JSON.parse(
|
|
26
|
+
fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8")
|
|
27
|
+
);
|
|
28
|
+
const optDeps = parentPkg.optionalDependencies || {};
|
|
29
|
+
for (const name of Object.keys(optDeps)) {
|
|
30
|
+
if (name.endsWith(`-${key}`)) {
|
|
31
|
+
return name;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
} catch (_) {}
|
|
35
|
+
|
|
36
|
+
return PLATFORM_PKG[key] || null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function resolveNativeBinary() {
|
|
40
|
+
const pkgName = getPlatformPackageName();
|
|
41
|
+
if (pkgName) {
|
|
42
|
+
try {
|
|
43
|
+
const pkgDir = path.dirname(require.resolve(`${pkgName}/package.json`));
|
|
44
|
+
const binPath = path.join(pkgDir, "bin", BINARY_FILENAME);
|
|
45
|
+
if (fs.existsSync(binPath)) {
|
|
46
|
+
return { path: binPath, fromPlatformPkg: true };
|
|
47
|
+
}
|
|
48
|
+
} catch (err) {
|
|
49
|
+
if (err.code !== "MODULE_NOT_FOUND") {
|
|
50
|
+
console.warn(`[WARN] Unexpected error resolving ${pkgName}: ${err.message}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const legacyPath = path.join(__dirname, "..", "bin", BINARY_FILENAME);
|
|
56
|
+
if (fs.existsSync(legacyPath)) {
|
|
57
|
+
return { path: legacyPath, fromPlatformPkg: false };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = {
|
|
64
|
+
IS_WINDOWS,
|
|
65
|
+
BINARY_FILENAME,
|
|
66
|
+
PLATFORM_PKG,
|
|
67
|
+
getPlatformPackageName,
|
|
68
|
+
resolveNativeBinary,
|
|
69
|
+
};
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
// Copyright 2026 alibaba/open-code-review Contributors
|
|
5
|
+
|
|
6
|
+
"use strict";
|
|
7
|
+
|
|
8
|
+
const fs = require("fs");
|
|
9
|
+
const path = require("path");
|
|
10
|
+
const os = require("os");
|
|
11
|
+
const https = require("https");
|
|
12
|
+
const { spawnSync } = require("child_process");
|
|
13
|
+
|
|
14
|
+
const { resolveNativeBinary } = require("./platform");
|
|
15
|
+
const { loadPackageJson } = require("./install.js");
|
|
16
|
+
const { SEMVER_RE, parseVersionOutput, semverGt } = require("./version");
|
|
17
|
+
|
|
18
|
+
const stateDir = path.join(os.homedir(), ".opencodereview");
|
|
19
|
+
const tsFile = path.join(stateDir, "last-update-check");
|
|
20
|
+
const lockFile = path.join(stateDir, "update.lock");
|
|
21
|
+
const hintFile = path.join(stateDir, "update-available");
|
|
22
|
+
|
|
23
|
+
const DEFAULT_REGISTRY = "https://registry.npmjs.org";
|
|
24
|
+
|
|
25
|
+
function touchTimestamp() {
|
|
26
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
27
|
+
const now = new Date();
|
|
28
|
+
try {
|
|
29
|
+
fs.utimesSync(tsFile, now, now);
|
|
30
|
+
} catch (_) {
|
|
31
|
+
fs.writeFileSync(tsFile, now.toISOString());
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function acquireLock() {
|
|
36
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
37
|
+
try {
|
|
38
|
+
fs.writeFileSync(lockFile, String(process.pid), { flag: "wx" });
|
|
39
|
+
return true;
|
|
40
|
+
} catch (e) {
|
|
41
|
+
if (e.code !== "EEXIST") return false;
|
|
42
|
+
try {
|
|
43
|
+
const pid = parseInt(fs.readFileSync(lockFile, "utf8").trim(), 10);
|
|
44
|
+
process.kill(pid, 0);
|
|
45
|
+
return false;
|
|
46
|
+
} catch (_) {
|
|
47
|
+
try {
|
|
48
|
+
fs.unlinkSync(lockFile);
|
|
49
|
+
fs.writeFileSync(lockFile, String(process.pid), { flag: "wx" });
|
|
50
|
+
return true;
|
|
51
|
+
} catch (_2) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function releaseLock() {
|
|
59
|
+
try {
|
|
60
|
+
fs.unlinkSync(lockFile);
|
|
61
|
+
} catch (_) {}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function getInstalledVersion(binPath) {
|
|
65
|
+
try {
|
|
66
|
+
const result = spawnSync(binPath, ["version"], {
|
|
67
|
+
encoding: "utf8",
|
|
68
|
+
timeout: 3000,
|
|
69
|
+
});
|
|
70
|
+
return parseVersionOutput(result.stdout);
|
|
71
|
+
} catch (_) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function fetchLatestVersion(pkg) {
|
|
77
|
+
const registry = (pkg.publishConfig && pkg.publishConfig.registry) || DEFAULT_REGISTRY;
|
|
78
|
+
const pkgName = pkg.name;
|
|
79
|
+
if (!pkgName) return Promise.resolve(null);
|
|
80
|
+
const encodedName = pkgName.replace(/\//g, "%2F");
|
|
81
|
+
const url = `${registry.replace(/\/$/, "")}/${encodedName}/latest`;
|
|
82
|
+
if (!url.startsWith("https://")) return Promise.resolve(null);
|
|
83
|
+
|
|
84
|
+
return new Promise((resolve) => {
|
|
85
|
+
const options = {
|
|
86
|
+
headers: { "User-Agent": "ocr-updater", Accept: "application/json" },
|
|
87
|
+
timeout: 15000,
|
|
88
|
+
};
|
|
89
|
+
const req = https
|
|
90
|
+
.get(url, options, (res) => {
|
|
91
|
+
if (res.statusCode !== 200) {
|
|
92
|
+
res.resume();
|
|
93
|
+
resolve(null);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
let data = "";
|
|
97
|
+
res.on("data", (chunk) => (data += chunk));
|
|
98
|
+
res.on("end", () => {
|
|
99
|
+
try {
|
|
100
|
+
const json = JSON.parse(data);
|
|
101
|
+
resolve(json.version || null);
|
|
102
|
+
} catch (_) {
|
|
103
|
+
resolve(null);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
res.on("error", () => resolve(null));
|
|
107
|
+
})
|
|
108
|
+
.on("error", () => resolve(null));
|
|
109
|
+
req.on("timeout", () => {
|
|
110
|
+
req.destroy();
|
|
111
|
+
resolve(null);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function writeHint(latestVersion, pkgName) {
|
|
117
|
+
try {
|
|
118
|
+
fs.writeFileSync(hintFile, JSON.stringify({ version: latestVersion, pkg: pkgName }));
|
|
119
|
+
} catch (_) {}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function removeHint() {
|
|
123
|
+
try {
|
|
124
|
+
fs.unlinkSync(hintFile);
|
|
125
|
+
} catch (_) {}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function main() {
|
|
129
|
+
touchTimestamp();
|
|
130
|
+
|
|
131
|
+
if (!acquireLock()) return;
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
const resolved = resolveNativeBinary();
|
|
135
|
+
if (!resolved) return;
|
|
136
|
+
|
|
137
|
+
const installedVersion = getInstalledVersion(resolved.path);
|
|
138
|
+
if (!installedVersion) return;
|
|
139
|
+
|
|
140
|
+
const pkg = loadPackageJson();
|
|
141
|
+
const latestVersion = await fetchLatestVersion(pkg);
|
|
142
|
+
if (!latestVersion) return;
|
|
143
|
+
|
|
144
|
+
if (!SEMVER_RE.test(latestVersion)) return;
|
|
145
|
+
|
|
146
|
+
if (!semverGt(latestVersion, installedVersion)) {
|
|
147
|
+
removeHint();
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const pkgName = pkg.name;
|
|
152
|
+
const IS_WINDOWS = process.platform === "win32";
|
|
153
|
+
const result = spawnSync("npm", ["i", "-g", `${pkgName}@${latestVersion}`], {
|
|
154
|
+
encoding: "utf8",
|
|
155
|
+
timeout: 120000,
|
|
156
|
+
shell: IS_WINDOWS,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
if (result.status === 0) {
|
|
160
|
+
removeHint();
|
|
161
|
+
} else {
|
|
162
|
+
writeHint(latestVersion, pkgName);
|
|
163
|
+
}
|
|
164
|
+
} catch (_) {
|
|
165
|
+
} finally {
|
|
166
|
+
releaseLock();
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
main().catch(() => {});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright 2026 alibaba/open-code-review Contributors
|
|
3
|
+
|
|
4
|
+
"use strict";
|
|
5
|
+
|
|
6
|
+
const SEMVER_RE =
|
|
7
|
+
/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
8
|
+
|
|
9
|
+
function parseVersionOutput(output) {
|
|
10
|
+
const match = String(output || "").match(
|
|
11
|
+
/v(\d+\.\d+(?:\.\d+)?(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?)/
|
|
12
|
+
);
|
|
13
|
+
return match ? match[1] : null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function semverGt(a, b) {
|
|
17
|
+
if (!SEMVER_RE.test(a) || !SEMVER_RE.test(b)) return false;
|
|
18
|
+
|
|
19
|
+
const aWithoutBuild = a.replace(/\+.*$/, "");
|
|
20
|
+
const bWithoutBuild = b.replace(/\+.*$/, "");
|
|
21
|
+
const pa = aWithoutBuild.replace(/-.*$/, "").split(".").map(Number);
|
|
22
|
+
const pb = bWithoutBuild.replace(/-.*$/, "").split(".").map(Number);
|
|
23
|
+
for (let i = 0; i < 3; i++) {
|
|
24
|
+
if (pa[i] > pb[i]) return true;
|
|
25
|
+
if (pa[i] < pb[i]) return false;
|
|
26
|
+
}
|
|
27
|
+
const aPre = aWithoutBuild.includes("-");
|
|
28
|
+
const bPre = bWithoutBuild.includes("-");
|
|
29
|
+
if (bPre && !aPre) return true;
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function shouldShowUpdateHint(hintVersion, installedVersion) {
|
|
34
|
+
if (!SEMVER_RE.test(hintVersion) || !SEMVER_RE.test(installedVersion)) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
return semverGt(hintVersion, installedVersion);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = {
|
|
41
|
+
SEMVER_RE,
|
|
42
|
+
parseVersionOutput,
|
|
43
|
+
semverGt,
|
|
44
|
+
shouldShowUpdateHint,
|
|
45
|
+
};
|