@tedbrain/vlink 0.1.38
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 +83 -0
- package/bin/vlink.js +47 -0
- package/binaries.json +12 -0
- package/install.js +417 -0
- package/installer.js +479 -0
- package/package.json +29 -0
package/README.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# @tedbrain/vlink
|
|
2
|
+
|
|
3
|
+
跨平台 vlink npm 安装包。包本身不包含 Rust 二进制,安装时会像 Claude Code /
|
|
4
|
+
Codex 一样按当前平台下载对应的 vlink 二进制。推荐统一使用 npx 完成持久化安装
|
|
5
|
+
和 TedBrain 初始化:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx -y @tedbrain/vlink@latest install
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
该命令会识别平台和架构、安装或更新 vlink、配置用户 PATH,并启动
|
|
12
|
+
`vlink setup tedbrain`。安装完成后可直接运行:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
vlink --version
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
也可以只把 npm 包作为临时 CLI 使用:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npx -y @tedbrain/vlink@latest --version
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## 下载地址
|
|
25
|
+
|
|
26
|
+
安装脚本优先从 Gitee Release 解析当前版本对应平台的附件并下载;Gitee 不可用、
|
|
27
|
+
release 不存在或校验失败时,自动回退到 d01 下载服务器:
|
|
28
|
+
|
|
29
|
+
```text
|
|
30
|
+
优先: https://gitee.com/ted-team/tedbrain 的 release attachment
|
|
31
|
+
回退: http://49.232.144.199/downloads/oss/vlink/<version>/vlink-<platform>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
可用环境变量覆盖:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# 跳过 Gitee,直接使用 d01/自定义下载源
|
|
38
|
+
VLINK_SKIP_GITEE=1 npx -y @tedbrain/vlink@latest install
|
|
39
|
+
|
|
40
|
+
# d01 回退源改用开发通道
|
|
41
|
+
VLINK_DOWNLOAD_CHANNEL=oss-dev npx -y @tedbrain/vlink@latest install
|
|
42
|
+
|
|
43
|
+
# 自定义回退下载根路径(会自动拼接 <channel>/vlink/<version>/vlink-<platform>)
|
|
44
|
+
VLINK_DOWNLOAD_BASE=https://internal.example/downloads npx -y @tedbrain/vlink@latest install
|
|
45
|
+
|
|
46
|
+
# 自定义回退下载目录(会自动拼接 vlink-<platform>)
|
|
47
|
+
VLINK_DOWNLOAD_URL=https://internal.example/vlink npx -y @tedbrain/vlink@latest install
|
|
48
|
+
|
|
49
|
+
# 指定不同版本(同时影响 Gitee release tag 与 d01 回退路径)
|
|
50
|
+
VLINK_VERSION=0.1.21 npx -y @tedbrain/vlink@latest install
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## 平台映射
|
|
54
|
+
|
|
55
|
+
| npm platform/arch | 下载文件 |
|
|
56
|
+
| --- | --- |
|
|
57
|
+
| linux/x64, glibc >= 2.17 | `vlink-linux-x86_64-glibc2.17` |
|
|
58
|
+
| linux/x64 | `vlink-linux-x86_64` |
|
|
59
|
+
| linux/arm64 | `vlink-linux-aarch64` |
|
|
60
|
+
| darwin/x64 | `vlink-macos-x86_64` |
|
|
61
|
+
| darwin/arm64 | `vlink-macos-aarch64` |
|
|
62
|
+
| win32/x64 | `vlink-windows-x86_64.exe` |
|
|
63
|
+
| win32/arm64 | `vlink-windows-aarch64.exe` |
|
|
64
|
+
|
|
65
|
+
## 二进制存放位置
|
|
66
|
+
|
|
67
|
+
- 优先写入 npm 包目录 `vendor/vlink`(或 `vlink.exe`)。
|
|
68
|
+
- 如果包目录不可写,则回退到 `~/.cache/vlink/<version>/`。
|
|
69
|
+
- 也可以直接指定 `VLINK_BINARY_PATH` 指向一个已存在的 vlink 二进制。
|
|
70
|
+
|
|
71
|
+
安装脚本会校验 SHA-256(与 `dist/*.manifest` 一致),并执行一次
|
|
72
|
+
`vlink -V` 验证可运行。
|
|
73
|
+
|
|
74
|
+
## 发布
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
cd biz/eda/packages/vlink/npm
|
|
78
|
+
./scripts/publish.sh --dry-run
|
|
79
|
+
./scripts/publish.sh
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
发布脚本会从 `Cargo.toml` 读取 version,动态生成 `package.json`,再以 public
|
|
83
|
+
访问级别执行 `npm pack`/`npm publish`;仓库不维护静态 `package.json`。
|
package/bin/vlink.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { spawn } = require('child_process');
|
|
5
|
+
const { ensureBinary } = require('../install');
|
|
6
|
+
const { runInstallerFromArgs } = require('../installer');
|
|
7
|
+
|
|
8
|
+
async function main() {
|
|
9
|
+
const args = process.argv.slice(2);
|
|
10
|
+
if (args[0] === 'install') {
|
|
11
|
+
await runInstallerFromArgs(args.slice(1));
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const binaryPath = await ensureBinary();
|
|
16
|
+
const child = spawn(binaryPath, args, { stdio: 'inherit' });
|
|
17
|
+
|
|
18
|
+
for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
|
|
19
|
+
process.on(signal, () => {
|
|
20
|
+
if (!child.killed) {
|
|
21
|
+
child.kill(signal);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
child.on('error', (err) => {
|
|
27
|
+
console.error(`[vlink] failed to start ${binaryPath}: ${err.message}`);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
child.on('exit', (code, signal) => {
|
|
32
|
+
if (signal) {
|
|
33
|
+
try {
|
|
34
|
+
process.kill(process.pid, signal);
|
|
35
|
+
} catch {
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
process.exit(code == null ? 1 : code);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
main().catch((err) => {
|
|
45
|
+
console.error(`[vlink] ${err.message}`);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
});
|
package/binaries.json
ADDED
package/install.js
ADDED
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const crypto = require('crypto');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const http = require('http');
|
|
7
|
+
const https = require('https');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const { spawnSync } = require('child_process');
|
|
11
|
+
|
|
12
|
+
const PKG_ROOT = path.resolve(__dirname);
|
|
13
|
+
const PKG = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, 'package.json'), 'utf8'));
|
|
14
|
+
const CHECKSUMS = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, 'binaries.json'), 'utf8'));
|
|
15
|
+
|
|
16
|
+
const VERSION = process.env.VLINK_VERSION || PKG.version;
|
|
17
|
+
const CHANNEL = process.env.VLINK_DOWNLOAD_CHANNEL || 'oss';
|
|
18
|
+
const DOWNLOAD_BASE = (process.env.VLINK_DOWNLOAD_BASE || 'http://49.232.144.199/downloads')
|
|
19
|
+
.replace(/\/+$/, '');
|
|
20
|
+
|
|
21
|
+
const GITEE_API_BASE = (
|
|
22
|
+
process.env.VLINK_GITEE_API_BASE || 'https://gitee.com/api/v5/repos/ted-team/tedbrain'
|
|
23
|
+
).replace(/\/+$/, '');
|
|
24
|
+
|
|
25
|
+
const GLIBC217_SUFFIX = 'linux-x86_64-glibc2.17';
|
|
26
|
+
|
|
27
|
+
class VlinkInstallError extends Error {}
|
|
28
|
+
|
|
29
|
+
function parseGlibcVersion(text) {
|
|
30
|
+
for (const line of String(text || '').split(/\r?\n/)) {
|
|
31
|
+
const glibcMatch = line.match(/\bglibc\s+([0-9]+(?:\.[0-9]+)+)/);
|
|
32
|
+
if (glibcMatch) return glibcMatch[1];
|
|
33
|
+
const versionMatch = line.match(/\b([0-9]+(?:\.[0-9]+)+)\b/);
|
|
34
|
+
if (versionMatch) return versionMatch[1];
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function glibcVersion(deps = {}) {
|
|
40
|
+
if (Object.prototype.hasOwnProperty.call(process.env, 'VLINK_GLIBC_VERSION')) {
|
|
41
|
+
return process.env.VLINK_GLIBC_VERSION || null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const run = deps.spawnSync || spawnSync;
|
|
45
|
+
const getconf = run('getconf', ['GNU_LIBC_VERSION'], {
|
|
46
|
+
encoding: 'utf8',
|
|
47
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
48
|
+
});
|
|
49
|
+
if (getconf && getconf.status === 0) {
|
|
50
|
+
const parsed = parseGlibcVersion(getconf.stdout);
|
|
51
|
+
if (parsed) return parsed;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const ldd = run('ldd', ['--version'], {
|
|
55
|
+
encoding: 'utf8',
|
|
56
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
57
|
+
});
|
|
58
|
+
if (ldd && ldd.status === 0) {
|
|
59
|
+
return parseGlibcVersion(`${ldd.stdout || ''}\n${ldd.stderr || ''}`);
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function versionAtLeast(actual, requiredMajor, requiredMinor) {
|
|
65
|
+
const match = String(actual || '').match(/^([0-9]+)\.([0-9]+)/);
|
|
66
|
+
if (!match) return false;
|
|
67
|
+
const major = Number(match[1]);
|
|
68
|
+
const minor = Number(match[2]);
|
|
69
|
+
return major > requiredMajor || (major === requiredMajor && minor >= requiredMinor);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function platformSpec(options = {}) {
|
|
73
|
+
const platform = options.platform || process.env.VLINK_PLATFORM || process.platform;
|
|
74
|
+
const arch = options.arch || process.env.VLINK_ARCH || process.arch;
|
|
75
|
+
const detectedGlibc =
|
|
76
|
+
options.glibcVersion !== undefined
|
|
77
|
+
? options.glibcVersion
|
|
78
|
+
: platform === 'linux' && arch === 'x64'
|
|
79
|
+
? glibcVersion(options)
|
|
80
|
+
: null;
|
|
81
|
+
|
|
82
|
+
const spec = { platform, arch, suffixes: [], binaryName: 'vlink' };
|
|
83
|
+
|
|
84
|
+
if (platform === 'linux') {
|
|
85
|
+
if (arch === 'x64') {
|
|
86
|
+
if (versionAtLeast(detectedGlibc, 2, 17)) {
|
|
87
|
+
spec.suffixes.push(GLIBC217_SUFFIX);
|
|
88
|
+
}
|
|
89
|
+
spec.suffixes.push('linux-x86_64');
|
|
90
|
+
} else if (arch === 'arm64') {
|
|
91
|
+
spec.suffixes.push('linux-aarch64');
|
|
92
|
+
}
|
|
93
|
+
} else if (platform === 'darwin') {
|
|
94
|
+
if (arch === 'x64') spec.suffixes.push('macos-x86_64');
|
|
95
|
+
else if (arch === 'arm64') spec.suffixes.push('macos-aarch64');
|
|
96
|
+
} else if (platform === 'win32') {
|
|
97
|
+
spec.binaryName = 'vlink.exe';
|
|
98
|
+
if (arch === 'x64') spec.suffixes.push('windows-x86_64.exe');
|
|
99
|
+
else if (arch === 'arm64') spec.suffixes.push('windows-aarch64.exe');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (spec.suffixes.length === 0) {
|
|
103
|
+
throw new VlinkInstallError(
|
|
104
|
+
`unsupported platform/architecture: ${platform}/${arch}`
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
spec.suffix = spec.suffixes[0];
|
|
108
|
+
return spec;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function fallbackDownloadUrl(spec, suffix = spec.suffix) {
|
|
112
|
+
const base = process.env.VLINK_DOWNLOAD_URL;
|
|
113
|
+
if (base) {
|
|
114
|
+
return `${base.replace(/\/+$/, '')}/vlink-${suffix}`;
|
|
115
|
+
}
|
|
116
|
+
return `${DOWNLOAD_BASE}/${CHANNEL}/vlink/${VERSION}/vlink-${suffix}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function httpGetText(url) {
|
|
120
|
+
return new Promise((resolve, reject) => {
|
|
121
|
+
const request = (target, redirects) => {
|
|
122
|
+
const transport = target.startsWith('https:') ? https : http;
|
|
123
|
+
const req = transport.get(
|
|
124
|
+
target,
|
|
125
|
+
{ headers: { 'user-agent': `${PKG.name}/${PKG.version}` } },
|
|
126
|
+
(res) => {
|
|
127
|
+
if (
|
|
128
|
+
res.statusCode >= 300 &&
|
|
129
|
+
res.statusCode < 400 &&
|
|
130
|
+
res.headers.location &&
|
|
131
|
+
redirects < 5
|
|
132
|
+
) {
|
|
133
|
+
res.resume();
|
|
134
|
+
const next = new URL(res.headers.location, target).toString();
|
|
135
|
+
return request(next, redirects + 1);
|
|
136
|
+
}
|
|
137
|
+
if (res.statusCode !== 200) {
|
|
138
|
+
res.resume();
|
|
139
|
+
return reject(
|
|
140
|
+
new VlinkInstallError(`HTTP ${res.statusCode} from ${target}`)
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const chunks = [];
|
|
145
|
+
res.on('data', (chunk) => chunks.push(chunk));
|
|
146
|
+
res.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
|
147
|
+
}
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
req.on('error', reject);
|
|
151
|
+
req.setTimeout(30000, () => req.destroy(new VlinkInstallError('request timed out')));
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
request(url, 0);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function parseJson(text, url) {
|
|
159
|
+
try {
|
|
160
|
+
return JSON.parse(text);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
throw new VlinkInstallError(`invalid JSON from ${url}: ${error.message}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function giteeReleaseId(version) {
|
|
167
|
+
const url = `${GITEE_API_BASE}/releases/tags/${version}`;
|
|
168
|
+
const release = parseJson(await httpGetText(url), url);
|
|
169
|
+
if (!release || typeof release !== 'object' || !Number.isInteger(release.id)) {
|
|
170
|
+
throw new VlinkInstallError(`gitee release is missing an id: ${url}`);
|
|
171
|
+
}
|
|
172
|
+
return release.id;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function giteeAttachmentDownloadUrl(spec, suffix = spec.suffix) {
|
|
176
|
+
const releaseId = await giteeReleaseId(VERSION);
|
|
177
|
+
const attachmentsUrl = `${GITEE_API_BASE}/releases/${releaseId}/attach_files?per_page=100`;
|
|
178
|
+
const attachments = parseJson(await httpGetText(attachmentsUrl), attachmentsUrl);
|
|
179
|
+
if (!Array.isArray(attachments)) {
|
|
180
|
+
throw new VlinkInstallError(`gitee attachments response is not an array: ${attachmentsUrl}`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const remoteName = `vlink-${suffix}`;
|
|
184
|
+
const attachment = attachments.find((item) => item && item.name === remoteName);
|
|
185
|
+
if (!attachment) {
|
|
186
|
+
throw new VlinkInstallError(`gitee release attachment not found: ${remoteName}`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (
|
|
190
|
+
typeof attachment.browser_download_url === 'string' &&
|
|
191
|
+
attachment.browser_download_url.trim() !== ''
|
|
192
|
+
) {
|
|
193
|
+
return attachment.browser_download_url.trim();
|
|
194
|
+
}
|
|
195
|
+
if (Number.isInteger(attachment.id)) {
|
|
196
|
+
return `${GITEE_API_BASE}/releases/${releaseId}/attach_files/${attachment.id}/download`;
|
|
197
|
+
}
|
|
198
|
+
throw new VlinkInstallError(`gitee attachment has no usable download URL: ${remoteName}`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function buildDownloadSources(spec, suffix = spec.suffix) {
|
|
202
|
+
const sources = [];
|
|
203
|
+
if (process.env.VLINK_SKIP_GITEE !== '1') {
|
|
204
|
+
sources.push({
|
|
205
|
+
name: 'gitee',
|
|
206
|
+
resolve: () => giteeAttachmentDownloadUrl(spec, suffix)
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
sources.push({
|
|
210
|
+
name: process.env.VLINK_DOWNLOAD_URL ? 'custom' : 'd01',
|
|
211
|
+
resolve: () => Promise.resolve(fallbackDownloadUrl(spec, suffix))
|
|
212
|
+
});
|
|
213
|
+
return sources;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function packageBinaryPath(spec) {
|
|
217
|
+
return path.join(PKG_ROOT, 'vendor', spec.binaryName);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function cacheBinaryPath(spec) {
|
|
221
|
+
const cacheRoot =
|
|
222
|
+
process.env.VLINK_BINARY_DIR ||
|
|
223
|
+
path.join(
|
|
224
|
+
process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache'),
|
|
225
|
+
'vlink',
|
|
226
|
+
VERSION
|
|
227
|
+
);
|
|
228
|
+
return path.join(cacheRoot, spec.binaryName);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function isExecutable(filePath) {
|
|
232
|
+
try {
|
|
233
|
+
fs.accessSync(filePath, fs.constants.X_OK);
|
|
234
|
+
return fs.statSync(filePath).isFile();
|
|
235
|
+
} catch {
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function isWritable(dir) {
|
|
241
|
+
try {
|
|
242
|
+
fs.accessSync(dir, fs.constants.W_OK);
|
|
243
|
+
return true;
|
|
244
|
+
} catch {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function resolveBinaryPath(spec) {
|
|
250
|
+
if (process.env.VLINK_BINARY_PATH) {
|
|
251
|
+
return process.env.VLINK_BINARY_PATH;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const primary = packageBinaryPath(spec);
|
|
255
|
+
if (isExecutable(primary)) return primary;
|
|
256
|
+
|
|
257
|
+
const cache = cacheBinaryPath(spec);
|
|
258
|
+
if (isExecutable(cache)) return cache;
|
|
259
|
+
|
|
260
|
+
if (isWritable(PKG_ROOT)) return primary;
|
|
261
|
+
return cache;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function expectedChecksum(spec, suffix = spec.suffix) {
|
|
265
|
+
if (VERSION !== PKG.version) return null;
|
|
266
|
+
if (CHECKSUMS.version && CHECKSUMS.version !== VERSION) return null;
|
|
267
|
+
const value = CHECKSUMS.binaries && CHECKSUMS.binaries[suffix];
|
|
268
|
+
return typeof value === 'string' && value.length === 64 ? value.toLowerCase() : null;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function sha256File(filePath) {
|
|
272
|
+
return new Promise((resolve, reject) => {
|
|
273
|
+
const hash = crypto.createHash('sha256');
|
|
274
|
+
const stream = fs.createReadStream(filePath);
|
|
275
|
+
stream.on('error', reject);
|
|
276
|
+
stream.on('data', (chunk) => hash.update(chunk));
|
|
277
|
+
stream.on('end', () => resolve(hash.digest('hex')));
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function downloadFile(url, destPath) {
|
|
282
|
+
return new Promise((resolve, reject) => {
|
|
283
|
+
const fileStream = fs.createWriteStream(destPath, { mode: 0o755 });
|
|
284
|
+
|
|
285
|
+
const finish = () => {
|
|
286
|
+
fileStream.close((closeErr) => {
|
|
287
|
+
if (closeErr) reject(closeErr);
|
|
288
|
+
else resolve();
|
|
289
|
+
});
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const request = (target, redirects) => {
|
|
293
|
+
const transport = target.startsWith('https:') ? https : http;
|
|
294
|
+
const req = transport.get(
|
|
295
|
+
target,
|
|
296
|
+
{ headers: { 'user-agent': `${PKG.name}/${PKG.version}` } },
|
|
297
|
+
(res) => {
|
|
298
|
+
if (
|
|
299
|
+
res.statusCode >= 300 &&
|
|
300
|
+
res.statusCode < 400 &&
|
|
301
|
+
res.headers.location &&
|
|
302
|
+
redirects < 5
|
|
303
|
+
) {
|
|
304
|
+
res.resume();
|
|
305
|
+
const next = new URL(res.headers.location, target).toString();
|
|
306
|
+
return request(next, redirects + 1);
|
|
307
|
+
}
|
|
308
|
+
if (res.statusCode !== 200) {
|
|
309
|
+
res.resume();
|
|
310
|
+
return reject(
|
|
311
|
+
new VlinkInstallError(`download failed: HTTP ${res.statusCode} from ${target}`)
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
res.pipe(fileStream);
|
|
315
|
+
}
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
req.on('error', reject);
|
|
319
|
+
req.setTimeout(300000, () => req.destroy(new VlinkInstallError('download timed out')));
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
fileStream.on('finish', finish);
|
|
323
|
+
fileStream.on('error', reject);
|
|
324
|
+
request(url, 0);
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function verifyBinary(binaryPath) {
|
|
329
|
+
const result = spawnSync(binaryPath, ['-V'], {
|
|
330
|
+
stdio: ['ignore', 'ignore', 'pipe'],
|
|
331
|
+
encoding: 'utf8',
|
|
332
|
+
timeout: 30000
|
|
333
|
+
});
|
|
334
|
+
if (result.error) {
|
|
335
|
+
throw new VlinkInstallError(`downloaded binary is not executable: ${result.error.message}`);
|
|
336
|
+
}
|
|
337
|
+
if (result.status !== 0) {
|
|
338
|
+
throw new VlinkInstallError(`downloaded binary failed its version check (exit ${result.status})`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async function ensureBinary({ force = false } = {}) {
|
|
343
|
+
const spec = platformSpec();
|
|
344
|
+
const binaryPath = resolveBinaryPath(spec);
|
|
345
|
+
if (!force && isExecutable(binaryPath)) {
|
|
346
|
+
return binaryPath;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const dir = path.dirname(binaryPath);
|
|
350
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
351
|
+
|
|
352
|
+
let lastError = new VlinkInstallError('no download source available');
|
|
353
|
+
|
|
354
|
+
for (const suffix of spec.suffixes) {
|
|
355
|
+
const tempPath = path.join(dir, `.${spec.binaryName}.download-${process.pid}`);
|
|
356
|
+
const sources = buildDownloadSources(spec, suffix);
|
|
357
|
+
for (const source of sources) {
|
|
358
|
+
try {
|
|
359
|
+
const url = await source.resolve();
|
|
360
|
+
console.error(`[vlink] downloading ${url} [${source.name}]`);
|
|
361
|
+
fs.rmSync(tempPath, { force: true });
|
|
362
|
+
await downloadFile(url, tempPath);
|
|
363
|
+
|
|
364
|
+
const checksum = expectedChecksum(spec, suffix);
|
|
365
|
+
if (checksum) {
|
|
366
|
+
const actual = await sha256File(tempPath);
|
|
367
|
+
if (actual !== checksum) {
|
|
368
|
+
throw new VlinkInstallError(
|
|
369
|
+
`checksum mismatch for ${suffix}: expected ${checksum}, got ${actual}`
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (process.platform !== 'win32') {
|
|
375
|
+
fs.chmodSync(tempPath, 0o755);
|
|
376
|
+
}
|
|
377
|
+
if (fs.existsSync(binaryPath)) fs.rmSync(binaryPath, { force: true });
|
|
378
|
+
fs.renameSync(tempPath, binaryPath);
|
|
379
|
+
verifyBinary(binaryPath);
|
|
380
|
+
return binaryPath;
|
|
381
|
+
} catch (err) {
|
|
382
|
+
lastError = err;
|
|
383
|
+
fs.rmSync(tempPath, { force: true });
|
|
384
|
+
console.error(`[vlink] ${source.name} download failed for ${suffix}: ${err.message}`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
throw new VlinkInstallError(`all download sources failed: ${lastError.message}`);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
module.exports = {
|
|
393
|
+
VlinkInstallError,
|
|
394
|
+
VERSION,
|
|
395
|
+
GLIBC217_SUFFIX,
|
|
396
|
+
parseGlibcVersion,
|
|
397
|
+
glibcVersion,
|
|
398
|
+
versionAtLeast,
|
|
399
|
+
platformSpec,
|
|
400
|
+
fallbackDownloadUrl,
|
|
401
|
+
giteeAttachmentDownloadUrl,
|
|
402
|
+
buildDownloadSources,
|
|
403
|
+
resolveBinaryPath,
|
|
404
|
+
ensureBinary,
|
|
405
|
+
verifyBinary
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
if (require.main === module) {
|
|
409
|
+
ensureBinary()
|
|
410
|
+
.then((binaryPath) => {
|
|
411
|
+
console.error(`[vlink] installed ${binaryPath}`);
|
|
412
|
+
})
|
|
413
|
+
.catch((err) => {
|
|
414
|
+
console.error(`[vlink] install failed: ${err.message}`);
|
|
415
|
+
process.exitCode = 1;
|
|
416
|
+
});
|
|
417
|
+
}
|
package/installer.js
ADDED
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { spawnSync } = require('child_process');
|
|
8
|
+
|
|
9
|
+
const installModule = require('./install');
|
|
10
|
+
|
|
11
|
+
const CURRENT_WRAPPER = path.resolve(__dirname, 'bin', 'vlink.js');
|
|
12
|
+
|
|
13
|
+
class VlinkSetupError extends Error {}
|
|
14
|
+
|
|
15
|
+
function log(message, output = process.stderr) {
|
|
16
|
+
output.write(`[TedBrain] ${message}\n`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function environmentValue(env, name) {
|
|
20
|
+
const expected = name.toLowerCase();
|
|
21
|
+
for (const key of Object.keys(env)) {
|
|
22
|
+
if (key.toLowerCase() === expected && env[key] !== undefined) {
|
|
23
|
+
return env[key];
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return '';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function defaultInstallDir(spec, env = process.env, home = os.homedir()) {
|
|
30
|
+
if (env.VLINK_INSTALL_DIR) return env.VLINK_INSTALL_DIR;
|
|
31
|
+
if (spec.platform === 'win32') {
|
|
32
|
+
const localAppData = env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
|
|
33
|
+
return path.join(localAppData, 'TedBrain', 'bin');
|
|
34
|
+
}
|
|
35
|
+
return path.join(home, '.local', 'bin');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function installPathFor(spec, installDir) {
|
|
39
|
+
return path.join(installDir, spec.binaryName);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isExecutableFile(filePath, platform = process.platform, fsImpl = fs) {
|
|
43
|
+
try {
|
|
44
|
+
if (!fsImpl.statSync(filePath).isFile()) return false;
|
|
45
|
+
if (platform === 'win32') return true;
|
|
46
|
+
fsImpl.accessSync(filePath, fs.constants.X_OK);
|
|
47
|
+
return true;
|
|
48
|
+
} catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isVlinkWrapper(filePath, fsImpl = fs) {
|
|
54
|
+
const normalized = path.resolve(filePath).replace(/\\/g, '/').toLowerCase();
|
|
55
|
+
if (normalized.includes('/node_modules/.bin/')) return true;
|
|
56
|
+
|
|
57
|
+
let realPath = normalized;
|
|
58
|
+
try {
|
|
59
|
+
realPath = fsImpl.realpathSync(filePath).replace(/\\/g, '/').toLowerCase();
|
|
60
|
+
} catch {
|
|
61
|
+
// The regular path is still useful when realpath is unavailable.
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (realPath === CURRENT_WRAPPER.replace(/\\/g, '/').toLowerCase()) return true;
|
|
65
|
+
if (/\/@(tedbrain|tedmind)\/vlink\/bin\/vlink\.js$/.test(realPath)) return true;
|
|
66
|
+
|
|
67
|
+
let fd;
|
|
68
|
+
try {
|
|
69
|
+
fd = fsImpl.openSync(filePath, 'r');
|
|
70
|
+
const buffer = Buffer.alloc(64 * 1024);
|
|
71
|
+
const length = fsImpl.readSync(fd, buffer, 0, buffer.length, 0);
|
|
72
|
+
const contents = buffer.subarray(0, length).toString('utf8');
|
|
73
|
+
return /@(tedbrain|tedmind)\/vlink\/bin\/vlink\.js/.test(contents);
|
|
74
|
+
} catch {
|
|
75
|
+
return false;
|
|
76
|
+
} finally {
|
|
77
|
+
if (fd !== undefined) {
|
|
78
|
+
try {
|
|
79
|
+
fsImpl.closeSync(fd);
|
|
80
|
+
} catch {
|
|
81
|
+
// Best-effort cleanup only.
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function findOnPath(binaryName, options = {}) {
|
|
88
|
+
const env = options.env || process.env;
|
|
89
|
+
const platform = options.platform || process.platform;
|
|
90
|
+
const fsImpl = options.fs || fs;
|
|
91
|
+
const rawPath = environmentValue(env, 'PATH');
|
|
92
|
+
const delimiter = platform === 'win32' ? ';' : path.delimiter;
|
|
93
|
+
|
|
94
|
+
for (const entry of String(rawPath).split(delimiter)) {
|
|
95
|
+
if (!entry) continue;
|
|
96
|
+
const candidate = path.join(entry, binaryName);
|
|
97
|
+
if (
|
|
98
|
+
isExecutableFile(candidate, platform, fsImpl) &&
|
|
99
|
+
!isVlinkWrapper(candidate, fsImpl)
|
|
100
|
+
) {
|
|
101
|
+
return candidate;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function prependEnvironmentPath(directory, env, platform) {
|
|
108
|
+
const delimiter = platform === 'win32' ? ';' : path.delimiter;
|
|
109
|
+
const current = environmentValue(env, 'PATH');
|
|
110
|
+
const entries = String(current)
|
|
111
|
+
.split(delimiter)
|
|
112
|
+
.filter(Boolean);
|
|
113
|
+
const alreadyPresent = entries.some((entry) => entry === directory);
|
|
114
|
+
const next = alreadyPresent ? entries.join(delimiter) : [directory, ...entries].join(delimiter);
|
|
115
|
+
|
|
116
|
+
for (const key of Object.keys(env)) {
|
|
117
|
+
if (key.toLowerCase() === 'path') delete env[key];
|
|
118
|
+
}
|
|
119
|
+
env.PATH = next;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function samePath(left, right, platform) {
|
|
123
|
+
const normalize = (value) => {
|
|
124
|
+
let normalized = path.resolve(value);
|
|
125
|
+
while (normalized.length > 1 && /[\\/]$/.test(normalized)) {
|
|
126
|
+
normalized = normalized.slice(0, -1);
|
|
127
|
+
}
|
|
128
|
+
return platform === 'win32' ? normalized.toLowerCase() : normalized;
|
|
129
|
+
};
|
|
130
|
+
return normalize(left) === normalize(right);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function updateWindowsUserPath(directory, options) {
|
|
134
|
+
const spawnSyncImpl = options.spawnSync || spawnSync;
|
|
135
|
+
const env = { ...options.env, TEDBRAIN_INSTALL_DIR: directory };
|
|
136
|
+
const script = [
|
|
137
|
+
"$entry = $env:TEDBRAIN_INSTALL_DIR.TrimEnd('\\')",
|
|
138
|
+
"$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')",
|
|
139
|
+
"$entries = @($userPath -split ';' | Where-Object { $_ })",
|
|
140
|
+
"if (-not ($entries | Where-Object { $_.TrimEnd('\\') -ieq $entry })) {",
|
|
141
|
+
" $newPath = (@($env:TEDBRAIN_INSTALL_DIR) + $entries) -join ';'",
|
|
142
|
+
" [Environment]::SetEnvironmentVariable('Path', $newPath, 'User')",
|
|
143
|
+
" Write-Output 'changed'",
|
|
144
|
+
"}"
|
|
145
|
+
].join('\n');
|
|
146
|
+
const result = spawnSyncImpl('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script], {
|
|
147
|
+
env,
|
|
148
|
+
encoding: 'utf8',
|
|
149
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
150
|
+
});
|
|
151
|
+
if (result.error) {
|
|
152
|
+
throw new VlinkSetupError(`failed to update the Windows user PATH: ${result.error.message}`);
|
|
153
|
+
}
|
|
154
|
+
if (result.status !== 0) {
|
|
155
|
+
const detail = String(result.stderr || '').trim();
|
|
156
|
+
throw new VlinkSetupError(
|
|
157
|
+
`failed to update the Windows user PATH${detail ? `: ${detail}` : ''}`
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
return String(result.stdout || '').trim() === 'changed';
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function ensureUserPath(installDir, options = {}) {
|
|
164
|
+
const env = options.env || process.env;
|
|
165
|
+
const platform = options.platform || process.platform;
|
|
166
|
+
const home = options.home || os.homedir();
|
|
167
|
+
const fsImpl = options.fs || fs;
|
|
168
|
+
const output = options.output || process.stderr;
|
|
169
|
+
|
|
170
|
+
prependEnvironmentPath(installDir, env, platform);
|
|
171
|
+
|
|
172
|
+
if (platform === 'win32') {
|
|
173
|
+
const changed = updateWindowsUserPath(installDir, {
|
|
174
|
+
...options,
|
|
175
|
+
env,
|
|
176
|
+
fs: fsImpl,
|
|
177
|
+
output
|
|
178
|
+
});
|
|
179
|
+
if (changed) log(`added ${installDir} to the user PATH`, output);
|
|
180
|
+
return { changed, persistent: true };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const defaultDir = path.join(home, '.local', 'bin');
|
|
184
|
+
if (!samePath(installDir, defaultDir, platform)) {
|
|
185
|
+
log(`using custom install directory; add ${installDir} to your shell PATH permanently`, output);
|
|
186
|
+
return { changed: false, persistent: false };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const shellName = path.basename(env.SHELL || 'bash');
|
|
190
|
+
let rcFile;
|
|
191
|
+
let pathLine;
|
|
192
|
+
switch (shellName) {
|
|
193
|
+
case 'zsh':
|
|
194
|
+
rcFile = path.join(home, '.zshrc');
|
|
195
|
+
pathLine = 'export PATH="$HOME/.local/bin:$PATH"';
|
|
196
|
+
break;
|
|
197
|
+
case 'fish':
|
|
198
|
+
rcFile = path.join(home, '.config', 'fish', 'config.fish');
|
|
199
|
+
pathLine = 'fish_add_path "$HOME/.local/bin"';
|
|
200
|
+
break;
|
|
201
|
+
case 'csh':
|
|
202
|
+
case 'tcsh':
|
|
203
|
+
rcFile = path.join(home, `.${shellName}rc`);
|
|
204
|
+
pathLine = 'setenv PATH "$HOME/.local/bin:$PATH"';
|
|
205
|
+
break;
|
|
206
|
+
default:
|
|
207
|
+
rcFile = path.join(home, '.bashrc');
|
|
208
|
+
pathLine = 'export PATH="$HOME/.local/bin:$PATH"';
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
fsImpl.mkdirSync(path.dirname(rcFile), { recursive: true });
|
|
213
|
+
const current = fsImpl.existsSync(rcFile) ? fsImpl.readFileSync(rcFile, 'utf8') : '';
|
|
214
|
+
if (current.split(/\r?\n/).includes(pathLine)) {
|
|
215
|
+
return { changed: false, persistent: true, rcFile };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const separator = current && !current.endsWith('\n') ? '\n' : '';
|
|
219
|
+
fsImpl.writeFileSync(rcFile, `${current}${separator}\n${pathLine}\n`);
|
|
220
|
+
log(`added ${installDir} to PATH in ${rcFile}`, output);
|
|
221
|
+
return { changed: true, persistent: true, rcFile };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function commandFailure(result, label) {
|
|
225
|
+
if (result.error) return `${label}: ${result.error.message}`;
|
|
226
|
+
if (result.status !== 0) {
|
|
227
|
+
const detail = String(result.stderr || '').trim();
|
|
228
|
+
return `${label} failed with exit code ${result.status}${detail ? `: ${detail}` : ''}`;
|
|
229
|
+
}
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function runVlinkUpdate(vlinkPath, options = {}) {
|
|
234
|
+
const spawnSyncImpl = options.spawnSync || spawnSync;
|
|
235
|
+
const check = spawnSyncImpl(vlinkPath, ['update', '--json'], {
|
|
236
|
+
encoding: 'utf8',
|
|
237
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
238
|
+
});
|
|
239
|
+
if (check.error || check.status !== 0) {
|
|
240
|
+
return { supported: false };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
let update;
|
|
244
|
+
try {
|
|
245
|
+
update = JSON.parse(String(check.stdout || '').trim());
|
|
246
|
+
} catch (error) {
|
|
247
|
+
throw new VlinkSetupError(
|
|
248
|
+
`vlink update returned invalid JSON: ${error.message}`
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (update.update_required === false) {
|
|
253
|
+
return { supported: true, updateRequired: false };
|
|
254
|
+
}
|
|
255
|
+
if (update.update_required !== true) {
|
|
256
|
+
throw new VlinkSetupError('vlink update returned invalid JSON: missing update_required');
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const apply = spawnSyncImpl(vlinkPath, ['update', '--apply', '--json'], {
|
|
260
|
+
encoding: 'utf8',
|
|
261
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
262
|
+
});
|
|
263
|
+
const failure = commandFailure(apply, 'vlink update --apply');
|
|
264
|
+
if (failure) throw new VlinkSetupError(failure);
|
|
265
|
+
|
|
266
|
+
let applied;
|
|
267
|
+
try {
|
|
268
|
+
applied = JSON.parse(String(apply.stdout || '').trim());
|
|
269
|
+
} catch (error) {
|
|
270
|
+
throw new VlinkSetupError(
|
|
271
|
+
`vlink update --apply returned invalid JSON: ${error.message}`
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
supported: true,
|
|
277
|
+
updateRequired: true,
|
|
278
|
+
installedPath:
|
|
279
|
+
typeof applied.installed_path === 'string' && applied.installed_path.trim()
|
|
280
|
+
? applied.installed_path.trim()
|
|
281
|
+
: null
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function installBinary(sourcePath, targetPath, options = {}) {
|
|
286
|
+
const platform = options.platform || process.platform;
|
|
287
|
+
const fsImpl = options.fs || fs;
|
|
288
|
+
const spawnSyncImpl = options.spawnSync || spawnSync;
|
|
289
|
+
const output = options.output || process.stderr;
|
|
290
|
+
|
|
291
|
+
if (samePath(sourcePath, targetPath, platform)) return targetPath;
|
|
292
|
+
fsImpl.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
293
|
+
|
|
294
|
+
if (options.stopDaemon) {
|
|
295
|
+
log('stopping the existing vlink daemon before replacement', output);
|
|
296
|
+
const stop = spawnSyncImpl(sourcePath, ['daemon', 'stop'], { stdio: 'inherit' });
|
|
297
|
+
const failure = commandFailure(stop, 'vlink daemon stop');
|
|
298
|
+
if (failure) throw new VlinkSetupError(failure);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const temporary = path.join(
|
|
302
|
+
path.dirname(targetPath),
|
|
303
|
+
`.${path.basename(targetPath)}.install-${process.pid}-${Date.now()}`
|
|
304
|
+
);
|
|
305
|
+
try {
|
|
306
|
+
fsImpl.copyFileSync(sourcePath, temporary);
|
|
307
|
+
if (platform !== 'win32') fsImpl.chmodSync(temporary, 0o755);
|
|
308
|
+
if (platform === 'win32' && fsImpl.existsSync(targetPath)) {
|
|
309
|
+
fsImpl.rmSync(targetPath, { force: true });
|
|
310
|
+
}
|
|
311
|
+
fsImpl.renameSync(temporary, targetPath);
|
|
312
|
+
} catch (error) {
|
|
313
|
+
fsImpl.rmSync(temporary, { force: true });
|
|
314
|
+
throw new VlinkSetupError(`failed to install vlink at ${targetPath}: ${error.message}`);
|
|
315
|
+
}
|
|
316
|
+
fsImpl.rmSync(temporary, { force: true });
|
|
317
|
+
log(`installed vlink at ${targetPath}`, output);
|
|
318
|
+
return targetPath;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function binaryIsStaticElf(binaryPath, options = {}) {
|
|
322
|
+
if ((options.platform || process.platform) !== 'linux') return false;
|
|
323
|
+
const spawnSyncImpl = options.spawnSync || spawnSync;
|
|
324
|
+
const result = spawnSyncImpl('file', [binaryPath], {
|
|
325
|
+
encoding: 'utf8',
|
|
326
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
327
|
+
});
|
|
328
|
+
if (result.error || result.status !== 0) return false;
|
|
329
|
+
const details = String(result.stdout || '');
|
|
330
|
+
return details.includes('ELF') && !details.includes('dynamically linked');
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async function migrateStaticVlink(vlinkPath, spec, options) {
|
|
334
|
+
if (spec.suffixes[0] !== installModule.GLIBC217_SUFFIX) return vlinkPath;
|
|
335
|
+
if (!binaryIsStaticElf(vlinkPath, options)) return vlinkPath;
|
|
336
|
+
|
|
337
|
+
log('migrating the installed static vlink to the glibc 2.17 build', options.output);
|
|
338
|
+
const sourcePath = await options.ensureBinary({ force: true });
|
|
339
|
+
return installBinary(sourcePath, vlinkPath, {
|
|
340
|
+
...options,
|
|
341
|
+
stopDaemon: true
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async function runInstaller(options = {}) {
|
|
346
|
+
const env = options.env || process.env;
|
|
347
|
+
const platform = options.platform || env.VLINK_PLATFORM || process.platform;
|
|
348
|
+
const arch = options.arch || env.VLINK_ARCH || process.arch;
|
|
349
|
+
const home = options.home || os.homedir();
|
|
350
|
+
const fsImpl = options.fs || fs;
|
|
351
|
+
const output = options.output || process.stderr;
|
|
352
|
+
const spawnSyncImpl = options.spawnSync || spawnSync;
|
|
353
|
+
const platformSpec = options.platformSpec || installModule.platformSpec;
|
|
354
|
+
const ensureBinary =
|
|
355
|
+
options.ensureBinary || ((settings) => installModule.ensureBinary(settings));
|
|
356
|
+
|
|
357
|
+
const spec = platformSpec({
|
|
358
|
+
platform,
|
|
359
|
+
arch,
|
|
360
|
+
glibcVersion:
|
|
361
|
+
options.glibcVersion !== undefined
|
|
362
|
+
? options.glibcVersion
|
|
363
|
+
: env.VLINK_GLIBC_VERSION,
|
|
364
|
+
spawnSync: spawnSyncImpl
|
|
365
|
+
});
|
|
366
|
+
const installDir = defaultInstallDir(spec, env, home);
|
|
367
|
+
const targetPath = installPathFor(spec, installDir);
|
|
368
|
+
const sharedOptions = {
|
|
369
|
+
env,
|
|
370
|
+
platform,
|
|
371
|
+
home,
|
|
372
|
+
fs: fsImpl,
|
|
373
|
+
output,
|
|
374
|
+
spawnSync: spawnSyncImpl
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
let vlinkPath = findOnPath(spec.binaryName, {
|
|
378
|
+
env,
|
|
379
|
+
platform,
|
|
380
|
+
fs: fsImpl
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
if (vlinkPath) {
|
|
384
|
+
log(`found vlink at ${vlinkPath}`, output);
|
|
385
|
+
} else if (isExecutableFile(targetPath, platform, fsImpl)) {
|
|
386
|
+
vlinkPath = targetPath;
|
|
387
|
+
ensureUserPath(installDir, sharedOptions);
|
|
388
|
+
log(`found vlink at ${vlinkPath}`, output);
|
|
389
|
+
} else {
|
|
390
|
+
log(`installing vlink ${installModule.VERSION}`, output);
|
|
391
|
+
const sourcePath = await ensureBinary({});
|
|
392
|
+
installBinary(sourcePath, targetPath, {
|
|
393
|
+
...sharedOptions,
|
|
394
|
+
stopDaemon: false
|
|
395
|
+
});
|
|
396
|
+
ensureUserPath(installDir, sharedOptions);
|
|
397
|
+
vlinkPath = targetPath;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const update = runVlinkUpdate(vlinkPath, { spawnSync: spawnSyncImpl });
|
|
401
|
+
if (update.supported) {
|
|
402
|
+
if (update.updateRequired) {
|
|
403
|
+
log('a newer vlink release is available; applying it', output);
|
|
404
|
+
if (
|
|
405
|
+
update.installedPath &&
|
|
406
|
+
isExecutableFile(update.installedPath, platform, fsImpl)
|
|
407
|
+
) {
|
|
408
|
+
vlinkPath = update.installedPath;
|
|
409
|
+
if (samePath(path.dirname(vlinkPath), installDir, platform)) {
|
|
410
|
+
ensureUserPath(installDir, sharedOptions);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
} else {
|
|
414
|
+
log('vlink is up to date', output);
|
|
415
|
+
}
|
|
416
|
+
} else {
|
|
417
|
+
log('installed vlink does not support update checks; installing the latest release', output);
|
|
418
|
+
const sourcePath = await ensureBinary({});
|
|
419
|
+
installBinary(sourcePath, targetPath, {
|
|
420
|
+
...sharedOptions,
|
|
421
|
+
stopDaemon: true
|
|
422
|
+
});
|
|
423
|
+
ensureUserPath(installDir, sharedOptions);
|
|
424
|
+
vlinkPath = targetPath;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
vlinkPath = await migrateStaticVlink(vlinkPath, spec, {
|
|
428
|
+
...sharedOptions,
|
|
429
|
+
ensureBinary
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
log('starting TedBrain Agent setup', output);
|
|
433
|
+
const setup = spawnSyncImpl(vlinkPath, ['setup', 'tedbrain'], {
|
|
434
|
+
stdio: 'inherit'
|
|
435
|
+
});
|
|
436
|
+
const failure = commandFailure(setup, 'vlink setup tedbrain');
|
|
437
|
+
if (failure) throw new VlinkSetupError(failure);
|
|
438
|
+
|
|
439
|
+
return { installDir, vlinkPath };
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function printInstallHelp(output = process.stdout) {
|
|
443
|
+
output.write(
|
|
444
|
+
[
|
|
445
|
+
'Usage: npx -y @tedbrain/vlink@latest install',
|
|
446
|
+
'',
|
|
447
|
+
'Installs or updates vlink, adds it to the user PATH, and starts',
|
|
448
|
+
'the TedBrain setup wizard.'
|
|
449
|
+
].join('\n') + '\n'
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
async function runInstallerFromArgs(args, options = {}) {
|
|
454
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
455
|
+
printInstallHelp(options.output || process.stdout);
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
if (args.length > 0) {
|
|
459
|
+
throw new VlinkSetupError(`unknown install option: ${args[0]}`);
|
|
460
|
+
}
|
|
461
|
+
return runInstaller(options);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
module.exports = {
|
|
465
|
+
VlinkSetupError,
|
|
466
|
+
defaultInstallDir,
|
|
467
|
+
installPathFor,
|
|
468
|
+
isExecutableFile,
|
|
469
|
+
isVlinkWrapper,
|
|
470
|
+
findOnPath,
|
|
471
|
+
prependEnvironmentPath,
|
|
472
|
+
ensureUserPath,
|
|
473
|
+
runVlinkUpdate,
|
|
474
|
+
installBinary,
|
|
475
|
+
binaryIsStaticElf,
|
|
476
|
+
runInstaller,
|
|
477
|
+
runInstallerFromArgs,
|
|
478
|
+
printInstallHelp
|
|
479
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tedbrain/vlink",
|
|
3
|
+
"version": "0.1.38",
|
|
4
|
+
"description": "vlink Virtuoso bridge CLI installer. Installs the matching platform binary at npm install time, like Claude Code / Codex.",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"bin": {
|
|
7
|
+
"vlink": "bin/vlink.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"install.js",
|
|
12
|
+
"installer.js",
|
|
13
|
+
"binaries.json",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"postinstall": "node install.js",
|
|
18
|
+
"test": "node --test test/*.test.js"
|
|
19
|
+
},
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=16"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"vlink",
|
|
25
|
+
"virtuoso",
|
|
26
|
+
"eda",
|
|
27
|
+
"skill"
|
|
28
|
+
]
|
|
29
|
+
}
|