@tedbrain/vlink 0.1.76 → 0.1.77

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 CHANGED
@@ -8,6 +8,17 @@ Codex 一样按当前平台下载对应的 vlink 二进制。推荐统一使用
8
8
  npx --registry=https://registry.npmmirror.com -y @tedbrain/vlink@latest install
9
9
  ```
10
10
 
11
+ npm 包只注册 `vlink-install` 命令;`vlink` 命令由安装后的原生程序提供。
12
+ `npx` 会自动选择包中唯一的命令入口,因此上面的安装命令保持不变。
13
+ 全局安装 npm 包只会注册 `vlink-install`;Windows 下 npm 会生成
14
+ `vlink-install`、`vlink-install.cmd`、`vlink-install.ps1` 三个入口。
15
+ 通过全局 npm 包安装原生程序时,执行:
16
+
17
+ ```bash
18
+ npm install -g @tedbrain/vlink@latest --registry=https://registry.npmmirror.com
19
+ vlink-install install
20
+ ```
21
+
11
22
  该命令会识别平台和架构,始终安装与当前 npm 包版本一致的 vlink,配置用户
12
23
  PATH,并启动 `vlink setup tedbrain`。如果 PATH 中已有原生 vlink,会优先更新该位置;
13
24
  旧文件或目录不可写时,自动回退到用户安装目录(Unix 为 `~/.local/bin`,Windows 为
@@ -54,7 +65,7 @@ package 形式,绕过 npx 的命令推断:
54
65
  ```bash
55
66
  npm exec --registry=https://registry.npmmirror.com \
56
67
  --package=@tedbrain/vlink@latest -- \
57
- vlink install --verbose
68
+ vlink-install install --verbose
58
69
  ```
59
70
 
60
71
  若显式形式可以输出,问题发生在本机 npx 的参数路由或缓存阶段;若仍无 wrapper
@@ -66,6 +77,47 @@ npm exec --registry=https://registry.npmmirror.com \
66
77
  npx --registry=https://registry.npmmirror.com -y @tedbrain/vlink@latest install --no-setup
67
78
  ```
68
79
 
80
+ ## 从旧版全局 npm 安装迁移
81
+
82
+ 旧版 npm 包会注册 `vlink`、`vlink.cmd`、`vlink.ps1` 入口。新版改为由 npm
83
+ 生成和管理 `vlink-install` 系列入口,不直接重命名历史脚本,避免保留失效目标
84
+ 或覆盖新版入口。
85
+
86
+ npm 的 `postinstall` 下载成功后,以及原生 `install` 流程成功后,会自动清理
87
+ 当前 PATH、npm 提供的 prefix 中的旧入口;原生安装还会检查安装目标目录。
88
+ 清理只针对调用或链接目标明确指向 `@tedbrain/vlink/bin/vlink.js` 或历史
89
+ `@tedmind/vlink/bin/vlink.js` 的 npm 启动文件,也能处理包已删除的脚本和失效链接。
90
+ 原生程序、转发到 `vlink.exe` 的 `vlink.cmd`、无关脚本及 `vlink-install`
91
+ 系列入口会保留。重复执行无需额外处理;遇到权限不足会保留文件并报告具体路径。
92
+
93
+ 已有旧入口或卸载残留时,可直接重新运行安装器:
94
+
95
+ ```bash
96
+ npx --registry=https://registry.npmmirror.com -y @tedbrain/vlink@latest install
97
+ ```
98
+
99
+ 使用 `npm install --ignore-scripts` 会跳过 `postinstall` 清理,可随后执行
100
+ `vlink-install install`。`npm uninstall -g @tedbrain/vlink` 会移除当前包管理的
101
+ `vlink-install` 入口,保留已经持久化安装的原生 vlink。npm 7 起不再支持
102
+ [卸载钩子](https://docs.npmjs.com/cli/v11/using-npm/scripts/#a-note-on-a-lack-of-npm-uninstall-scripts),
103
+ 因此历史孤立入口由上述安装流程修复。
104
+
105
+ 完成后重新打开终端。Windows 可以用以下命令检查实际命中的入口:
106
+
107
+ ```powershell
108
+ Get-Command vlink -All | Format-Table CommandType, Name, Source -AutoSize
109
+ ```
110
+
111
+ 应优先命中原生安装目录中的 `vlink.exe` 或转发到它的 `vlink.cmd`。使用 nvm
112
+ 切换 Node 版本后,其他版本目录里的旧 npm 入口也可能重新出现;需要在对应
113
+ 版本下重新运行安装器。不会递归搜索当前 PATH 和 npm prefix 之外的其他 Node
114
+ 安装目录;无法自动清理的文件会在安装日志中列出。
115
+ 当前窗口可直接使用原生程序的完整路径:
116
+
117
+ ```powershell
118
+ & "$env:LOCALAPPDATA\TedBrain\bin\vlink.exe" --version
119
+ ```
120
+
69
121
  ## 安装 Web 通道
70
122
 
71
123
  用于网页接入、终端和协议命令调用时,执行:
package/binaries.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
- "version": "0.1.76",
2
+ "version": "0.1.77",
3
3
  "binaries": {
4
- "linux-x86_64": "03ecd407f13911ffbe63af7fdc97f98a0925ca2fc8283414c0c03ed1b0cee7c6",
5
- "linux-x86_64-glibc2.17": "b5a58a3d2415e5e48f6c640e782fc8f0f61b062ebe0987787f3f67dae9705057",
6
- "linux-aarch64": "75c7e079cc999938a60e53db08067007d9d06d9419af56539d1d93b521d26d07",
7
- "macos-x86_64": "100342baaa28c36499b4378d5b433c09c7c7d287de381cb118a11616e36a8293",
8
- "macos-aarch64": "b5d7d6ad04b3e925b02d58d4a590b5df3d6dc912c639bb8227ce2173b0700c04",
9
- "windows-x86_64.exe": "240ee12509e9044a4bfc89b931f18e5fc15369ae01b2fd547f279d90b82174ac",
10
- "windows-aarch64.exe": "a31ad59bd7d3279cb9e3cdeb08dfad513f4d0aa3028e49b31a0fa56291bf7585"
4
+ "linux-x86_64": "34b6523d3ae77305c78aac9d8d1a3519cd5fc87144a8dd7279255fbe6601e486",
5
+ "linux-x86_64-glibc2.17": "4f2b0cac4ae9d581505f11cc4ed82a6b3759985a9e58dc1031aa5075690b3d72",
6
+ "linux-aarch64": "ef3ba7b89ad9a6d8f119723e1d221ddeb4e1a07f63306fd61ef031f309a477a8",
7
+ "macos-x86_64": "53c8176ba9b1b2c0ebec3421ca544b167b7cf767cb90988434137ac85fc4d9b5",
8
+ "macos-aarch64": "c017542b6850381c9f3da83654293d3515d794fe03f7a595786a849f43dc3eaa",
9
+ "windows-x86_64.exe": "6e22d3983d359375873908f6bde9b13f1f2dc149510f8c1ab4666429f3f6c77d",
10
+ "windows-aarch64.exe": "dc5691a3a162fd4279210e8f70ce4f0a84fb0e5cb90865368dd79df684c86caa"
11
11
  }
12
12
  }
package/install.js CHANGED
@@ -11,6 +11,8 @@ const { spawnSync } = require('child_process');
11
11
  const { HttpProxyAgent } = require('http-proxy-agent');
12
12
  const { HttpsProxyAgent } = require('https-proxy-agent');
13
13
 
14
+ const { cleanupLegacyNpmShims } = require('./npm-shims');
15
+
14
16
  const PKG_ROOT = path.resolve(__dirname);
15
17
  const PKG = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, 'package.json'), 'utf8'));
16
18
  const CHECKSUMS = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, 'binaries.json'), 'utf8'));
@@ -572,6 +574,7 @@ module.exports = {
572
574
  if (require.main === module) {
573
575
  ensureBinary()
574
576
  .then((binaryPath) => {
577
+ cleanupLegacyNpmShims();
575
578
  console.error(`[vlink] installed ${binaryPath}`);
576
579
  })
577
580
  .catch((err) => {
package/installer.js CHANGED
@@ -7,6 +7,7 @@ const path = require('path');
7
7
  const { spawnSync } = require('child_process');
8
8
 
9
9
  const installModule = require('./install');
10
+ const { cleanupLegacyNpmShims } = require('./npm-shims');
10
11
 
11
12
  const PERMISSION_ERRORS = new Set(['EACCES', 'EPERM', 'EROFS']);
12
13
 
@@ -85,7 +86,7 @@ function isVlinkWrapper(filePath, fsImpl = fs) {
85
86
  fd = fsImpl.openSync(filePath, 'r');
86
87
  const buffer = Buffer.alloc(64 * 1024);
87
88
  const length = fsImpl.readSync(fd, buffer, 0, buffer.length, 0);
88
- const contents = buffer.subarray(0, length).toString('utf8');
89
+ const contents = buffer.subarray(0, length).toString('utf8').replace(/\\/g, '/');
89
90
  return /@(tedbrain|tedmind)\/vlink\/bin\/vlink\.js/.test(contents);
90
91
  } catch {
91
92
  return false;
@@ -593,6 +594,10 @@ async function runInstaller(options = {}) {
593
594
  log('Web channel is ready', output);
594
595
  }
595
596
 
597
+ cleanupLegacyNpmShims({
598
+ ...sharedOptions,
599
+ directories: [installDir, path.dirname(vlinkPath)]
600
+ });
596
601
  log(`installation complete: ${vlinkPath}`, output);
597
602
  log('open a new terminal to use the updated PATH', output);
598
603
  return { installDir, vlinkPath, commandShimPath, setupSkipped: skipSetup, webStarted: web };
@@ -602,6 +607,7 @@ function printInstallHelp(output = process.stdout) {
602
607
  output.write(
603
608
  [
604
609
  'Usage: npx --registry=https://registry.npmmirror.com -y @tedbrain/vlink@latest install [--no-setup] [--web] [--verbose]',
610
+ ' vlink-install install [--no-setup] [--web] [--verbose]',
605
611
  '',
606
612
  'Installs or updates vlink and adds it to the user PATH.',
607
613
  '',
package/npm-shims.js ADDED
@@ -0,0 +1,92 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const LEGACY_COMMANDS = ['vlink', 'vlink.cmd', 'vlink.ps1'];
7
+ const MAX_SHIM_BYTES = 64 * 1024;
8
+ const PACKAGE_WRAPPER = /\/node_modules\/@(?:tedbrain|tedmind)\/vlink\/bin\/vlink\.js$/i;
9
+ const MISSING_PATH_ERRORS = new Set(['ENOENT', 'ENOTDIR']);
10
+
11
+ function environmentValue(env, name) {
12
+ const key = Object.keys(env).find((key) => key.toLowerCase() === name.toLowerCase());
13
+ return key ? String(env[key] || '') : '';
14
+ }
15
+
16
+ function isPackageWrapper(target) {
17
+ return PACKAGE_WRAPPER.test(target.replace(/\\/g, '/'));
18
+ }
19
+
20
+ function shimTarget(contents, extension) {
21
+ // Match the invocation, not mentions of the package in comments or other text.
22
+ for (let line of contents.replace(/\\/g, '/').split(/\r?\n/)) {
23
+ line = line.trim();
24
+ let match;
25
+ if (extension === '.cmd') {
26
+ line = line.replace(/^endLocal & goto #_undefined_# 2>NUL \|\| title %COMSPEC% & /i, '');
27
+ match = line.match(/^@?(?:"(?:%_prog%|(?:%~dp0|%dp0%)\/?node(?:\.exe)?|node(?:\.exe)?)"|node(?:\.exe)?)\s+"%(?:~dp0|dp0%)\/?([^"]+)"\s+%\*$/i);
28
+ } else if (extension === '.ps1') {
29
+ match = line.match(/^(?:\$input\s*\|\s*)?&\s+"(?:\$basedir\/)?node(?:\$exe|\.exe)?"\s+"\$basedir\/([^"]+)"\s+\$args$/i);
30
+ } else {
31
+ match = line.match(/^exec\s+(?:"\$basedir\/node(?:\.exe)?"|node(?:\.exe)?)\s+"\$basedir\/([^"]+)"\s+"\$@"$/);
32
+ }
33
+ if (match) return match[1];
34
+ }
35
+ return null;
36
+ }
37
+
38
+ function isNpmVlinkShim(filePath, fsImpl = fs) {
39
+ const stat = fsImpl.lstatSync(filePath);
40
+ if (stat.isSymbolicLink()) {
41
+ // readlink also identifies orphaned Unix bin links after package removal.
42
+ return isPackageWrapper(path.resolve(path.dirname(filePath), fsImpl.readlinkSync(filePath)));
43
+ }
44
+ if (!stat.isFile() || stat.size > MAX_SHIM_BYTES) return false;
45
+
46
+ const contents = fsImpl.readFileSync(filePath, 'utf8');
47
+ if (contents.includes('\0')) return false;
48
+ const target = shimTarget(contents, path.extname(filePath).toLowerCase());
49
+ return target !== null && isPackageWrapper(path.resolve(path.dirname(filePath), target));
50
+ }
51
+
52
+ function cleanupLegacyNpmShims(options = {}) {
53
+ const env = options.env || process.env;
54
+ const platform = options.platform || process.platform;
55
+ const fsImpl = options.fs || fs;
56
+ const output = options.output || process.stderr;
57
+ const directories = environmentValue(env, 'PATH').split(platform === 'win32' ? ';' : ':');
58
+ const prefix = environmentValue(env, 'npm_config_prefix');
59
+ if (prefix) directories.push(platform === 'win32' ? prefix : path.join(prefix, 'bin'));
60
+ directories.push(env.VLINK_INSTALL_DIR, ...(options.directories || []));
61
+
62
+ const visited = new Set();
63
+ const removed = [];
64
+ const failed = [];
65
+ for (let directory of directories) {
66
+ if (!directory) continue;
67
+ if (platform === 'win32') directory = directory.replace(/^"(.*)"$/, '$1');
68
+ if (!directory) continue;
69
+ directory = path.resolve(directory);
70
+ const key = platform === 'win32' ? directory.toLowerCase() : directory;
71
+ if (visited.has(key)) continue;
72
+ visited.add(key);
73
+
74
+ for (const name of LEGACY_COMMANDS) {
75
+ const candidate = path.join(directory, name);
76
+ try {
77
+ if (!isNpmVlinkShim(candidate, fsImpl)) continue;
78
+ // Never recurse or follow the link when removing an npm-owned entry.
79
+ fsImpl.unlinkSync(candidate);
80
+ removed.push(candidate);
81
+ output.write(`[vlink] removed legacy npm shim: ${candidate}\n`);
82
+ } catch (error) {
83
+ if (MISSING_PATH_ERRORS.has(error.code)) continue;
84
+ failed.push(candidate);
85
+ output.write(`[vlink] warning: cannot clean legacy npm shim ${candidate}: ${error.message}\n`);
86
+ }
87
+ }
88
+ }
89
+ return { removed, failed };
90
+ }
91
+
92
+ module.exports = { cleanupLegacyNpmShims, isNpmVlinkShim };
package/package.json CHANGED
@@ -1,15 +1,16 @@
1
1
  {
2
2
  "name": "@tedbrain/vlink",
3
- "version": "0.1.76",
3
+ "version": "0.1.77",
4
4
  "description": "vlink Virtuoso bridge CLI installer. Installs the matching platform binary at npm install time, like Claude Code / Codex.",
5
5
  "license": "UNLICENSED",
6
6
  "bin": {
7
- "vlink": "bin/vlink.js"
7
+ "vlink-install": "bin/vlink.js"
8
8
  },
9
9
  "files": [
10
10
  "bin/",
11
11
  "install.js",
12
12
  "installer.js",
13
+ "npm-shims.js",
13
14
  "binaries.json",
14
15
  "README.md"
15
16
  ],