@tedbrain/vlink 0.1.63 → 0.1.65

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
@@ -37,6 +37,29 @@ npx --registry=https://registry.npmmirror.com -y @tedbrain/vlink@latest --versio
37
37
 
38
38
  安装命令会输出安装目标、权限回退原因及 setup 状态;失败时输出错误并返回非零退出码。
39
39
 
40
+ 需要排查安装过程时,加上 `--verbose`:
41
+
42
+ ```bash
43
+ npx --registry=https://registry.npmmirror.com -y \
44
+ @tedbrain/vlink@latest install --verbose
45
+ ```
46
+
47
+ verbose 会从 npm 包的 JavaScript wrapper 开始输出 Node、包版本、平台选择、PATH
48
+ 查找、权限检查、二进制缓存或下载、复制、补全和 setup 各阶段。也可以设置
49
+ `VLINK_VERBOSE=1`。日志不会打印完整环境变量、registry 凭据或下载 URL 的查询参数。
50
+
51
+ 如果上面的命令连 `[TedBrain:verbose] wrapper entered` 都没有输出,使用 npm 的显式
52
+ package 形式,绕过 npx 的命令推断:
53
+
54
+ ```bash
55
+ npm exec --registry=https://registry.npmmirror.com \
56
+ --package=@tedbrain/vlink@latest -- \
57
+ vlink install --verbose
58
+ ```
59
+
60
+ 若显式形式可以输出,问题发生在本机 npx 的参数路由或缓存阶段;若仍无 wrapper
61
+ 输出,可再加 npm 自身的 `--loglevel verbose` 检查 npm 在执行包之前的行为。
62
+
40
63
  手动配置 Agent 时可以只安装或更新 vlink,并跳过 TedBrain 配置向导:
41
64
 
42
65
  ```bash
@@ -53,6 +76,20 @@ release 不存在或校验失败时,自动回退到 d01 下载服务器:
53
76
  回退: http://49.232.144.199/downloads/oss/vlink/<version>/vlink-<platform>
54
77
  ```
55
78
 
79
+ 下载器遵循标准代理环境变量,同时支持大写和小写形式:
80
+
81
+ ```bash
82
+ HTTP_PROXY=http://proxy.example:8080
83
+ HTTPS_PROXY=http://proxy.example:8080
84
+ NO_PROXY=localhost,127.0.0.1,.internal.example
85
+ ```
86
+
87
+ 也支持 `http_proxy`、`https_proxy`、`no_proxy`、`ALL_PROXY`,以及 npm 注入的
88
+ `npm_config_proxy`、`npm_config_https_proxy` 和 `npm_config_noproxy`。小写变量与
89
+ 大写变量同时存在时优先使用小写。HTTPS 请求通过代理的 CONNECT 隧道传输;每次
90
+ 重定向都会根据目标地址重新应用代理和 `NO_PROXY`。verbose 日志仅显示脱敏后的
91
+ 代理主机,不显示代理用户名、密码或下载 URL 查询参数。
92
+
56
93
  可用环境变量覆盖:
57
94
 
58
95
  ```bash
package/bin/vlink.js CHANGED
@@ -2,12 +2,45 @@
2
2
  'use strict';
3
3
 
4
4
  const { spawn } = require('child_process');
5
- const { ensureBinary } = require('../install');
6
- const { runInstallerFromArgs } = require('../installer');
5
+
6
+ function verboseEnabled(args, env = process.env) {
7
+ const value = String(env.VLINK_VERBOSE || '').toLowerCase();
8
+ return args[0] === 'install' && (
9
+ args.includes('--verbose') || ['1', 'true', 'yes', 'on'].includes(value)
10
+ );
11
+ }
12
+
13
+ function verbose(message) {
14
+ process.stderr.write(`[TedBrain:verbose] ${message}\n`);
15
+ }
16
+
17
+ function diagnosticArgs(args) {
18
+ const safe = new Set(['install', '--verbose', '--no-setup', '--help', '-h']);
19
+ return args.map((arg) => {
20
+ if (safe.has(arg)) return arg;
21
+ if (arg.startsWith('--')) return arg.split('=', 1)[0];
22
+ return '<argument>';
23
+ });
24
+ }
7
25
 
8
26
  async function main() {
9
27
  const args = process.argv.slice(2);
28
+ const diagnostic = verboseEnabled(args);
29
+ if (diagnostic) {
30
+ process.env.VLINK_VERBOSE = '1';
31
+ verbose('wrapper entered');
32
+ verbose(`node=${process.version} executable=${process.execPath}`);
33
+ verbose(`wrapper=${__filename}`);
34
+ verbose(`cwd=${process.cwd()}`);
35
+ verbose(`argv=${JSON.stringify(diagnosticArgs(args))}`);
36
+ }
37
+
38
+ const { ensureBinary, VERSION } = require('../install');
39
+ const { runInstallerFromArgs } = require('../installer');
40
+ if (diagnostic) verbose(`package version=${VERSION}`);
41
+
10
42
  if (args[0] === 'install') {
43
+ if (diagnostic) verbose('dispatching install command');
11
44
  await runInstallerFromArgs(args.slice(1));
12
45
  return;
13
46
  }
package/binaries.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
- "version": "0.1.63",
2
+ "version": "0.1.65",
3
3
  "binaries": {
4
- "linux-x86_64": "dffb9115be72e1b1546e5d5adc9a208de7ac869f89cfc1692edb8ef60a05e8d1",
5
- "linux-x86_64-glibc2.17": "0acfdaec9e600817d55a389b9889c476dbfa9842768c9e431ef3c44df7e0fa5a",
6
- "linux-aarch64": "f8a3a086cf963db72d380e410c5264dc25659c44f3770597bc36d3021bf2376c",
7
- "macos-x86_64": "bc917cf21d802dc804acdcbd06eb769cd520a12475b25db9e8238036db7383ed",
8
- "macos-aarch64": "14b831e1f7ff7b09def2efc34e74d0a05d437cd1650c1c6f3c254cd08fc48b64",
9
- "windows-x86_64.exe": "c9ca055f7791b095f8cc958ffa0fab4d01eb0fa9b36801e25a51982ceeaf30fd",
10
- "windows-aarch64.exe": "717d470b48a10b1a16d897e56738447b3b7d6128cf02ea8f428e481117e80cd4"
4
+ "linux-x86_64": "8cfd3a34f9e8ddde2e14a27343fb8db8d3992ef3a0716fd19bf57f2c6ea8ef40",
5
+ "linux-x86_64-glibc2.17": "f74562c2b1c8a05f215d9ac05585fa70434136a77fb2c2525d90f0ebc60e1ed6",
6
+ "linux-aarch64": "bb2f87265642d333e3989218bec30ba2cc608f8bcf691cfd3033247c08c40533",
7
+ "macos-x86_64": "17906134f4d045e1177698589c46dfdd94cec249d51e8da2f7189f1e42d711b9",
8
+ "macos-aarch64": "051b1032ed78b2fe70c84debd2a27c69a7110d5703aee3dcf412e8393f915814",
9
+ "windows-x86_64.exe": "5b774f8ca3cf3416c5dbefa745ba7b652ef3291427c86e01b33175734892b9d2",
10
+ "windows-aarch64.exe": "85fe354aa315ffc1391b32e941dcfa954f67c6a1bc8ffb1ffaa292048e8cf864"
11
11
  }
12
12
  }
package/install.js CHANGED
@@ -8,6 +8,8 @@ const https = require('https');
8
8
  const os = require('os');
9
9
  const path = require('path');
10
10
  const { spawnSync } = require('child_process');
11
+ const { HttpProxyAgent } = require('http-proxy-agent');
12
+ const { HttpsProxyAgent } = require('https-proxy-agent');
11
13
 
12
14
  const PKG_ROOT = path.resolve(__dirname);
13
15
  const PKG = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, 'package.json'), 'utf8'));
@@ -26,6 +28,132 @@ const GLIBC217_SUFFIX = 'linux-x86_64-glibc2.17';
26
28
 
27
29
  class VlinkInstallError extends Error {}
28
30
 
31
+ function verboseEnabled(options = {}) {
32
+ if (options.verbose === true) return true;
33
+ return ['1', 'true', 'yes', 'on'].includes(
34
+ String(process.env.VLINK_VERBOSE || '').toLowerCase()
35
+ );
36
+ }
37
+
38
+ function verboseLog(message, options = {}) {
39
+ if (!verboseEnabled(options)) return;
40
+ (options.output || process.stderr).write(`[vlink:verbose] ${message}\n`);
41
+ }
42
+
43
+ function displayUrl(value) {
44
+ try {
45
+ const url = new URL(value);
46
+ url.username = '';
47
+ url.password = '';
48
+ url.search = '';
49
+ url.hash = '';
50
+ return url.toString();
51
+ } catch {
52
+ return '<invalid-url>';
53
+ }
54
+ }
55
+
56
+ function firstEnvironmentValue(env, names) {
57
+ for (const name of names) {
58
+ if (Object.prototype.hasOwnProperty.call(env, name) && env[name]) {
59
+ return String(env[name]).trim();
60
+ }
61
+ }
62
+ return '';
63
+ }
64
+
65
+ function normalizeProxyUrl(value) {
66
+ if (!value) return '';
67
+ const candidate = /^[a-z][a-z0-9+.-]*:\/\//i.test(value)
68
+ ? value
69
+ : `http://${value}`;
70
+ try {
71
+ return new URL(candidate).toString();
72
+ } catch (error) {
73
+ throw new VlinkInstallError(`invalid proxy URL: ${error.message}`);
74
+ }
75
+ }
76
+
77
+ function noProxyMatches(target, value) {
78
+ if (!value) return false;
79
+ const url = new URL(target);
80
+ const hostname = url.hostname.toLowerCase().replace(/^\[|\]$/g, '');
81
+ const port = url.port || (url.protocol === 'https:' ? '443' : '80');
82
+
83
+ return String(value).split(/[\s,]+/).filter(Boolean).some((entry) => {
84
+ if (entry === '*') return true;
85
+ let rule = entry.toLowerCase();
86
+ let rulePort = '';
87
+ if (rule.startsWith('[')) {
88
+ const end = rule.indexOf(']');
89
+ if (end !== -1 && rule[end + 1] === ':') {
90
+ rulePort = rule.slice(end + 2);
91
+ rule = rule.slice(1, end);
92
+ } else if (end !== -1) {
93
+ rule = rule.slice(1, end);
94
+ }
95
+ } else {
96
+ const colon = rule.lastIndexOf(':');
97
+ if (colon > -1 && rule.indexOf(':') === colon && /^\d+$/.test(rule.slice(colon + 1))) {
98
+ rulePort = rule.slice(colon + 1);
99
+ rule = rule.slice(0, colon);
100
+ }
101
+ }
102
+ if (rulePort && rulePort !== port) return false;
103
+ if (rule.startsWith('*')) rule = rule.slice(1);
104
+ if (rule.startsWith('.')) {
105
+ return hostname.endsWith(rule) || hostname === rule.slice(1);
106
+ }
107
+ return hostname === rule || hostname.endsWith(`.${rule}`);
108
+ });
109
+ }
110
+
111
+ function proxyForUrl(target, env = process.env) {
112
+ const noProxy = firstEnvironmentValue(env, [
113
+ 'no_proxy',
114
+ 'NO_PROXY',
115
+ 'npm_config_noproxy',
116
+ 'npm_config_no_proxy'
117
+ ]);
118
+ if (noProxyMatches(target, noProxy)) return '';
119
+
120
+ const protocol = new URL(target).protocol;
121
+ const names = protocol === 'https:'
122
+ ? [
123
+ 'https_proxy',
124
+ 'HTTPS_PROXY',
125
+ 'npm_config_https_proxy',
126
+ 'npm_config_proxy',
127
+ 'http_proxy',
128
+ 'HTTP_PROXY',
129
+ 'all_proxy',
130
+ 'ALL_PROXY'
131
+ ]
132
+ : [
133
+ 'http_proxy',
134
+ 'HTTP_PROXY',
135
+ 'npm_config_proxy',
136
+ 'all_proxy',
137
+ 'ALL_PROXY'
138
+ ];
139
+ return normalizeProxyUrl(firstEnvironmentValue(env, names));
140
+ }
141
+
142
+ function requestAgent(target, options = {}) {
143
+ const proxy = proxyForUrl(target, options.env || process.env);
144
+ if (!proxy) {
145
+ verboseLog(`network stage: direct ${displayUrl(target)}`, options);
146
+ return undefined;
147
+ }
148
+ verboseLog(
149
+ `network stage: ${displayUrl(target)} via proxy ${displayUrl(proxy)}`,
150
+ options
151
+ );
152
+ return target.startsWith('https:')
153
+ ? new HttpsProxyAgent(proxy)
154
+ : new HttpProxyAgent(proxy);
155
+ }
156
+
29
157
  function parseGlibcVersion(text) {
30
158
  for (const line of String(text || '').split(/\r?\n/)) {
31
159
  const glibcMatch = line.match(/\bglibc\s+([0-9]+(?:\.[0-9]+)+)/);
@@ -116,13 +244,16 @@ function fallbackDownloadUrl(spec, suffix = spec.suffix) {
116
244
  return `${DOWNLOAD_BASE}/${CHANNEL}/vlink/${VERSION}/vlink-${suffix}`;
117
245
  }
118
246
 
119
- function httpGetText(url) {
247
+ function httpGetText(url, options = {}) {
120
248
  return new Promise((resolve, reject) => {
121
249
  const request = (target, redirects) => {
122
250
  const transport = target.startsWith('https:') ? https : http;
123
251
  const req = transport.get(
124
252
  target,
125
- { headers: { 'user-agent': `${PKG.name}/${PKG.version}` } },
253
+ {
254
+ agent: requestAgent(target, options),
255
+ headers: { 'user-agent': `${PKG.name}/${PKG.version}` }
256
+ },
126
257
  (res) => {
127
258
  if (
128
259
  res.statusCode >= 300 &&
@@ -137,7 +268,7 @@ function httpGetText(url) {
137
268
  if (res.statusCode !== 200) {
138
269
  res.resume();
139
270
  return reject(
140
- new VlinkInstallError(`HTTP ${res.statusCode} from ${target}`)
271
+ new VlinkInstallError(`HTTP ${res.statusCode} from ${displayUrl(target)}`)
141
272
  );
142
273
  }
143
274
 
@@ -163,19 +294,22 @@ function parseJson(text, url) {
163
294
  }
164
295
  }
165
296
 
166
- async function giteeReleaseId(version) {
297
+ async function giteeReleaseId(version, options = {}) {
167
298
  const url = `${GITEE_API_BASE}/releases/tags/${version}`;
168
- const release = parseJson(await httpGetText(url), url);
299
+ const release = parseJson(await httpGetText(url, options), url);
169
300
  if (!release || typeof release !== 'object' || !Number.isInteger(release.id)) {
170
301
  throw new VlinkInstallError(`gitee release is missing an id: ${url}`);
171
302
  }
172
303
  return release.id;
173
304
  }
174
305
 
175
- async function giteeAttachmentDownloadUrl(spec, suffix = spec.suffix) {
176
- const releaseId = await giteeReleaseId(VERSION);
306
+ async function giteeAttachmentDownloadUrl(spec, suffix = spec.suffix, options = {}) {
307
+ const releaseId = await giteeReleaseId(VERSION, options);
177
308
  const attachmentsUrl = `${GITEE_API_BASE}/releases/${releaseId}/attach_files?per_page=100`;
178
- const attachments = parseJson(await httpGetText(attachmentsUrl), attachmentsUrl);
309
+ const attachments = parseJson(
310
+ await httpGetText(attachmentsUrl, options),
311
+ attachmentsUrl
312
+ );
179
313
  if (!Array.isArray(attachments)) {
180
314
  throw new VlinkInstallError(`gitee attachments response is not an array: ${attachmentsUrl}`);
181
315
  }
@@ -198,12 +332,12 @@ async function giteeAttachmentDownloadUrl(spec, suffix = spec.suffix) {
198
332
  throw new VlinkInstallError(`gitee attachment has no usable download URL: ${remoteName}`);
199
333
  }
200
334
 
201
- function buildDownloadSources(spec, suffix = spec.suffix) {
335
+ function buildDownloadSources(spec, suffix = spec.suffix, options = {}) {
202
336
  const sources = [];
203
337
  if (process.env.VLINK_SKIP_GITEE !== '1') {
204
338
  sources.push({
205
339
  name: 'gitee',
206
- resolve: () => giteeAttachmentDownloadUrl(spec, suffix)
340
+ resolve: () => giteeAttachmentDownloadUrl(spec, suffix, options)
207
341
  });
208
342
  }
209
343
  sources.push({
@@ -278,7 +412,7 @@ function sha256File(filePath) {
278
412
  });
279
413
  }
280
414
 
281
- function downloadFile(url, destPath) {
415
+ function downloadFile(url, destPath, options = {}) {
282
416
  return new Promise((resolve, reject) => {
283
417
  const fileStream = fs.createWriteStream(destPath, { mode: 0o755 });
284
418
 
@@ -293,7 +427,10 @@ function downloadFile(url, destPath) {
293
427
  const transport = target.startsWith('https:') ? https : http;
294
428
  const req = transport.get(
295
429
  target,
296
- { headers: { 'user-agent': `${PKG.name}/${PKG.version}` } },
430
+ {
431
+ agent: requestAgent(target, options),
432
+ headers: { 'user-agent': `${PKG.name}/${PKG.version}` }
433
+ },
297
434
  (res) => {
298
435
  if (
299
436
  res.statusCode >= 300 &&
@@ -308,7 +445,9 @@ function downloadFile(url, destPath) {
308
445
  if (res.statusCode !== 200) {
309
446
  res.resume();
310
447
  return reject(
311
- new VlinkInstallError(`download failed: HTTP ${res.statusCode} from ${target}`)
448
+ new VlinkInstallError(
449
+ `download failed: HTTP ${res.statusCode} from ${displayUrl(target)}`
450
+ )
312
451
  );
313
452
  }
314
453
  res.pipe(fileStream);
@@ -339,30 +478,46 @@ function verifyBinary(binaryPath) {
339
478
  }
340
479
  }
341
480
 
342
- async function ensureBinary({ force = false } = {}) {
481
+ async function ensureBinary(options = {}) {
482
+ const force = options.force === true;
343
483
  const spec = platformSpec();
344
484
  const binaryPath = resolveBinaryPath(spec);
485
+ verboseLog(
486
+ `binary resolver: version=${VERSION} platform=${spec.platform}/${spec.arch} ` +
487
+ `suffixes=${spec.suffixes.join(',')}`,
488
+ options
489
+ );
490
+ verboseLog(`binary resolver: selected path=${binaryPath} force=${force}`, options);
345
491
  if (!force && isExecutable(binaryPath)) {
492
+ verboseLog('binary resolver: using existing executable', options);
346
493
  return binaryPath;
347
494
  }
348
495
 
349
496
  const dir = path.dirname(binaryPath);
497
+ verboseLog(`binary resolver: creating download directory ${dir}`, options);
350
498
  fs.mkdirSync(dir, { recursive: true });
351
499
 
352
500
  let lastError = new VlinkInstallError('no download source available');
353
501
 
354
502
  for (const suffix of spec.suffixes) {
355
503
  const tempPath = path.join(dir, `.${spec.binaryName}.download-${process.pid}`);
356
- const sources = buildDownloadSources(spec, suffix);
504
+ const sources = buildDownloadSources(spec, suffix, options);
505
+ verboseLog(
506
+ `download stage: suffix=${suffix} sources=${sources.map((source) => source.name).join(',')}`,
507
+ options
508
+ );
357
509
  for (const source of sources) {
358
510
  try {
511
+ verboseLog(`download stage: resolving ${source.name}`, options);
359
512
  const url = await source.resolve();
360
- console.error(`[vlink] downloading ${url} [${source.name}]`);
513
+ console.error(`[vlink] downloading ${displayUrl(url)} [${source.name}]`);
361
514
  fs.rmSync(tempPath, { force: true });
362
- await downloadFile(url, tempPath);
515
+ await downloadFile(url, tempPath, options);
516
+ verboseLog(`download stage: received ${source.name} payload`, options);
363
517
 
364
518
  const checksum = expectedChecksum(spec, suffix);
365
519
  if (checksum) {
520
+ verboseLog(`verification stage: checking SHA-256 for ${suffix}`, options);
366
521
  const actual = await sha256File(tempPath);
367
522
  if (actual !== checksum) {
368
523
  throw new VlinkInstallError(
@@ -376,7 +531,9 @@ async function ensureBinary({ force = false } = {}) {
376
531
  }
377
532
  if (fs.existsSync(binaryPath)) fs.rmSync(binaryPath, { force: true });
378
533
  fs.renameSync(tempPath, binaryPath);
534
+ verboseLog(`verification stage: executing ${binaryPath} -V`, options);
379
535
  verifyBinary(binaryPath);
536
+ verboseLog('verification stage: binary is ready', options);
380
537
  return binaryPath;
381
538
  } catch (err) {
382
539
  lastError = err;
@@ -393,6 +550,12 @@ module.exports = {
393
550
  VlinkInstallError,
394
551
  VERSION,
395
552
  GLIBC217_SUFFIX,
553
+ verboseEnabled,
554
+ displayUrl,
555
+ noProxyMatches,
556
+ proxyForUrl,
557
+ requestAgent,
558
+ httpGetText,
396
559
  parseGlibcVersion,
397
560
  glibcVersion,
398
561
  versionAtLeast,
@@ -400,6 +563,7 @@ module.exports = {
400
563
  fallbackDownloadUrl,
401
564
  giteeAttachmentDownloadUrl,
402
565
  buildDownloadSources,
566
+ downloadFile,
403
567
  resolveBinaryPath,
404
568
  ensureBinary,
405
569
  verifyBinary
package/installer.js CHANGED
@@ -18,6 +18,20 @@ function log(message, output = process.stderr) {
18
18
  output.write(`[TedBrain] ${message}\n`);
19
19
  }
20
20
 
21
+ function verboseEnabled(options = {}) {
22
+ if (options.verbose === true) return true;
23
+ const env = options.env || process.env;
24
+ return ['1', 'true', 'yes', 'on'].includes(
25
+ String(env.VLINK_VERBOSE || '').toLowerCase()
26
+ );
27
+ }
28
+
29
+ function verboseLog(message, options = {}) {
30
+ if (!verboseEnabled(options)) return;
31
+ const output = options.output || process.stderr;
32
+ output.write(`[TedBrain:verbose] ${message}\n`);
33
+ }
34
+
21
35
  function environmentValue(env, name) {
22
36
  const expected = name.toLowerCase();
23
37
  for (const key of Object.keys(env)) {
@@ -170,6 +184,7 @@ function ensureUserPath(installDir, options = {}) {
170
184
  const fsImpl = options.fs || fs;
171
185
  const output = options.output || process.stderr;
172
186
 
187
+ verboseLog(`PATH stage: prioritizing ${installDir}`, options);
173
188
  prependEnvironmentPath(installDir, env, platform);
174
189
 
175
190
  if (platform === 'win32') {
@@ -291,7 +306,11 @@ function installBinary(sourcePath, targetPath, options = {}) {
291
306
  const spawnSyncImpl = options.spawnSync || spawnSync;
292
307
  const output = options.output || process.stderr;
293
308
 
294
- if (samePath(sourcePath, targetPath, platform)) return targetPath;
309
+ if (samePath(sourcePath, targetPath, platform)) {
310
+ verboseLog(`copy stage: source already equals target ${targetPath}`, options);
311
+ return targetPath;
312
+ }
313
+ verboseLog(`copy stage: ${sourcePath} -> ${targetPath}`, options);
295
314
  fsImpl.mkdirSync(path.dirname(targetPath), { recursive: true });
296
315
 
297
316
  if (options.stopDaemon) {
@@ -365,6 +384,7 @@ function installWindowsCommandShim(vlinkPath, options = {}) {
365
384
  function installShellCompletion(vlinkPath, options = {}) {
366
385
  const spawnSyncImpl = options.spawnSync || spawnSync;
367
386
  const output = options.output || process.stderr;
387
+ verboseLog(`completion stage: executing ${vlinkPath}`, options);
368
388
  const result = spawnSyncImpl(
369
389
  vlinkPath,
370
390
  ['completion', '--install', '--quiet'],
@@ -400,7 +420,11 @@ async function migrateStaticVlink(vlinkPath, spec, options) {
400
420
  if (!binaryIsStaticElf(vlinkPath, options)) return vlinkPath;
401
421
 
402
422
  log('migrating the installed static vlink to the glibc 2.17 build', options.output);
403
- const sourcePath = await options.ensureBinary({ force: true });
423
+ const sourcePath = await options.ensureBinary({
424
+ force: true,
425
+ verbose: verboseEnabled(options),
426
+ output: options.output
427
+ });
404
428
  return installBinary(sourcePath, vlinkPath, {
405
429
  ...options,
406
430
  stopDaemon: true
@@ -412,6 +436,7 @@ async function runInstaller(options = {}) {
412
436
  const platform = options.platform || env.VLINK_PLATFORM || process.platform;
413
437
  const arch = options.arch || env.VLINK_ARCH || process.arch;
414
438
  const skipSetup = Boolean(options.skipSetup);
439
+ const verbose = verboseEnabled(options);
415
440
  const home = options.home || os.homedir();
416
441
  const fsImpl = options.fs || fs;
417
442
  const output = options.output || process.stderr;
@@ -420,6 +445,17 @@ async function runInstaller(options = {}) {
420
445
  const ensureBinary =
421
446
  options.ensureBinary || ((settings) => installModule.ensureBinary(settings));
422
447
 
448
+ verboseLog(
449
+ `installer entered: package=${installModule.VERSION} platform=${platform} arch=${arch}`,
450
+ { ...options, env, output, verbose }
451
+ );
452
+ verboseLog(`runtime: node=${process.version} executable=${process.execPath}`, {
453
+ ...options,
454
+ env,
455
+ output,
456
+ verbose
457
+ });
458
+
423
459
  const spec = platformSpec({
424
460
  platform,
425
461
  arch,
@@ -437,14 +473,32 @@ async function runInstaller(options = {}) {
437
473
  home,
438
474
  fs: fsImpl,
439
475
  output,
440
- spawnSync: spawnSyncImpl
476
+ spawnSync: spawnSyncImpl,
477
+ verbose
441
478
  };
442
479
 
480
+ verboseLog(
481
+ `platform stage: suffixes=${spec.suffixes.join(',')} binary=${spec.binaryName}`,
482
+ sharedOptions
483
+ );
484
+ verboseLog(
485
+ `target stage: installDir=${installDir} configured=${Boolean(env.VLINK_INSTALL_DIR)}`,
486
+ sharedOptions
487
+ );
488
+ const pathEntries = String(environmentValue(env, 'PATH'))
489
+ .split(platform === 'win32' ? ';' : path.delimiter)
490
+ .filter(Boolean).length;
491
+ verboseLog(`PATH stage: inspecting ${pathEntries} entries`, sharedOptions);
492
+
443
493
  let vlinkPath = findOnPath(spec.binaryName, {
444
494
  env,
445
495
  platform,
446
496
  fs: fsImpl
447
497
  });
498
+ verboseLog(
499
+ `discovery stage: native PATH binary=${vlinkPath || 'none'}`,
500
+ sharedOptions
501
+ );
448
502
 
449
503
  if (vlinkPath) {
450
504
  log(
@@ -465,9 +519,11 @@ async function runInstaller(options = {}) {
465
519
  let installTarget = env.VLINK_INSTALL_DIR ? targetPath : (vlinkPath || targetPath);
466
520
  // Avoid trying to replace a shared installation, even if its parent is writable.
467
521
  if (vlinkPath && !env.VLINK_INSTALL_DIR && !samePath(vlinkPath, targetPath, platform)) {
522
+ verboseLog(`permission stage: checking ${vlinkPath} and its parent directory`, sharedOptions);
468
523
  try {
469
524
  fsImpl.accessSync(vlinkPath, fs.constants.W_OK);
470
525
  fsImpl.accessSync(path.dirname(vlinkPath), fs.constants.W_OK);
526
+ verboseLog('permission stage: existing installation is replaceable', sharedOptions);
471
527
  } catch (error) {
472
528
  if (!PERMISSION_ERRORS.has(error.code)) throw error;
473
529
  log(`cannot replace ${vlinkPath} (${error.code}); falling back to ${targetPath}`, output);
@@ -476,7 +532,9 @@ async function runInstaller(options = {}) {
476
532
  }
477
533
  installTarget = path.resolve(installTarget);
478
534
  log(`installation target: ${installTarget}`, output);
479
- const sourcePath = await ensureBinary({});
535
+ verboseLog('binary stage: resolving packaged or cached binary', sharedOptions);
536
+ const sourcePath = await ensureBinary({ verbose, output });
537
+ verboseLog(`binary stage: source=${sourcePath}`, sharedOptions);
480
538
  try {
481
539
  installBinary(sourcePath, installTarget, {
482
540
  ...sharedOptions,
@@ -500,18 +558,24 @@ async function runInstaller(options = {}) {
500
558
  ensureBinary
501
559
  });
502
560
  const commandShimPath = installWindowsCommandShim(vlinkPath, sharedOptions);
561
+ verboseLog(
562
+ `shim stage: ${commandShimPath || 'not required on this platform'}`,
563
+ sharedOptions
564
+ );
503
565
  installShellCompletion(vlinkPath, sharedOptions);
504
566
 
505
567
  if (skipSetup) {
506
568
  log('skipping TedBrain Agent setup', output);
507
569
  } else {
508
570
  log('starting TedBrain Agent setup', output);
571
+ verboseLog(`setup stage: executing ${vlinkPath} setup tedbrain`, sharedOptions);
509
572
  const setup = spawnSyncImpl(vlinkPath, ['setup', 'tedbrain'], {
510
573
  env,
511
574
  stdio: 'inherit'
512
575
  });
513
576
  const failure = commandFailure(setup, 'vlink setup tedbrain');
514
577
  if (failure) throw new VlinkSetupError(failure);
578
+ verboseLog('setup stage: completed successfully', sharedOptions);
515
579
  }
516
580
 
517
581
  log(`installation complete: ${vlinkPath}`, output);
@@ -522,11 +586,12 @@ async function runInstaller(options = {}) {
522
586
  function printInstallHelp(output = process.stdout) {
523
587
  output.write(
524
588
  [
525
- 'Usage: npx --registry=https://registry.npmmirror.com -y @tedbrain/vlink@latest install [--no-setup]',
589
+ 'Usage: npx --registry=https://registry.npmmirror.com -y @tedbrain/vlink@latest install [--no-setup] [--verbose]',
526
590
  '',
527
591
  'Installs or updates vlink and adds it to the user PATH.',
528
592
  '',
529
- ' --no-setup Install without starting the TedBrain setup wizard.'
593
+ ' --no-setup Install without starting the TedBrain setup wizard.',
594
+ ' --verbose Print diagnostics for every installation stage.'
530
595
  ].join('\n') + '\n'
531
596
  );
532
597
  }
@@ -538,18 +603,23 @@ async function runInstallerFromArgs(args, options = {}) {
538
603
  }
539
604
 
540
605
  let skipSetup = false;
606
+ let verbose = verboseEnabled(options);
541
607
  for (const arg of args) {
542
608
  if (arg === '--no-setup') {
543
609
  skipSetup = true;
610
+ } else if (arg === '--verbose') {
611
+ verbose = true;
544
612
  } else {
545
- throw new VlinkSetupError(`unknown install option: ${arg}`);
613
+ const displayArg = arg.startsWith('--') ? arg.split('=', 1)[0] : '<argument>';
614
+ throw new VlinkSetupError(`unknown install option: ${displayArg}`);
546
615
  }
547
616
  }
548
- return runInstaller({ ...options, skipSetup });
617
+ return runInstaller({ ...options, skipSetup, verbose });
549
618
  }
550
619
 
551
620
  module.exports = {
552
621
  VlinkSetupError,
622
+ verboseEnabled,
553
623
  defaultInstallDir,
554
624
  installPathFor,
555
625
  isExecutableFile,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tedbrain/vlink",
3
- "version": "0.1.63",
3
+ "version": "0.1.65",
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": {
@@ -17,6 +17,10 @@
17
17
  "postinstall": "node install.js",
18
18
  "test": "node --test test/*.test.js"
19
19
  },
20
+ "dependencies": {
21
+ "http-proxy-agent": "^7.0.2",
22
+ "https-proxy-agent": "^7.0.6"
23
+ },
20
24
  "engines": {
21
25
  "node": ">=16"
22
26
  },