@tedbrain/vlink 0.1.62 → 0.1.64

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
@@ -9,7 +9,14 @@ npx --registry=https://registry.npmmirror.com -y @tedbrain/vlink@latest install
9
9
  ```
10
10
 
11
11
  该命令会识别平台和架构,始终安装与当前 npm 包版本一致的 vlink,配置用户
12
- PATH,并启动 `vlink setup tedbrain`。安装完成后可直接运行:
12
+ PATH,并启动 `vlink setup tedbrain`。如果 PATH 中已有原生 vlink,会优先更新该位置;
13
+ 旧文件或目录不可写时,自动回退到用户安装目录(Unix 为 `~/.local/bin`,Windows 为
14
+ `%LOCALAPPDATA%\TedBrain\bin`),保留原来的共享安装。显式设置 `VLINK_INSTALL_DIR`
15
+ 时始终使用该目录,写入失败会报错,不会再改装到其他位置。
16
+
17
+ 安装目录会优先加入当前安装进程的 PATH,补全和 setup 均使用新二进制的绝对路径。
18
+ 默认目录还会写入 shell 配置或 Windows 用户 PATH;自定义 Unix 目录需要自行配置
19
+ 永久 PATH。完成后请打开新终端,让 PATH 和 shell 命令缓存更新,再运行:
13
20
 
14
21
  ```bash
15
22
  vlink --version
@@ -28,6 +35,31 @@ Windows 会在 `vlink.exe` 旁同时生成 `vlink.cmd`。安装目录加入用
28
35
  npx --registry=https://registry.npmmirror.com -y @tedbrain/vlink@latest --version
29
36
  ```
30
37
 
38
+ 安装命令会输出安装目标、权限回退原因及 setup 状态;失败时输出错误并返回非零退出码。
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
+
31
63
  手动配置 Agent 时可以只安装或更新 vlink,并跳过 TedBrain 配置向导:
32
64
 
33
65
  ```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.62",
2
+ "version": "0.1.64",
3
3
  "binaries": {
4
- "linux-x86_64": "367f723806e56bf5e5b882093aa3963045f743038e94a03163a1472912e24f63",
5
- "linux-x86_64-glibc2.17": "6e72ba9f87e7f28d8ff7316be91b611ab3b9213503626623aaaa67458b6b5804",
6
- "linux-aarch64": "a949d6df1361d2387a86cdf9849552020a3a2158e9a010da244cf4344fb86141",
7
- "macos-x86_64": "e26767ec171771221b8421a02e1c349dc3633bfc8ae1fc7b921e0f502d4fab0a",
8
- "macos-aarch64": "d59088bfd940376fa7fcea054e31ccf632ea2553a043da5d417d463deaf6429c",
9
- "windows-x86_64.exe": "01482e74fa89c6536ab75278636d2dfab861c69f4e8e2060dc5a3977a2f9b8b6",
10
- "windows-aarch64.exe": "572b403ac1ee1dcfd6dbcf8135c28f23fa7dfeca92fe1e958f7560ebc661cad0"
4
+ "linux-x86_64": "cd46ed189a1cb56a4d2ff84af7cda6a4870ad202f6172500e02a29654cb82b54",
5
+ "linux-x86_64-glibc2.17": "d578c70bafe98de520b25e78ea10919a12cb14fff0524ab3339bb0d912a78961",
6
+ "linux-aarch64": "8acbcce5bd84e14652f778ab987abe2a9f461b3ba4bb0dbe4022f8900a42868e",
7
+ "macos-x86_64": "5e96d897d75dc02a0e8acca05bfb8a5910751d38b10de0eda9f1c0ae9ac6df13",
8
+ "macos-aarch64": "8481807cd40cac885908550959e259164eaf9d2a20f10e2972c57a5f3e125815",
9
+ "windows-x86_64.exe": "90a2a090f993bc6258ccddacf62aa05d99d96b411a39a041cf2e6f19c6f8fd69",
10
+ "windows-aarch64.exe": "e96d0dcbfde313747a62cb7b5f945cfdf444927f40e758f25ed06da6c0d517b6"
11
11
  }
12
12
  }
package/install.js CHANGED
@@ -26,6 +26,31 @@ const GLIBC217_SUFFIX = 'linux-x86_64-glibc2.17';
26
26
 
27
27
  class VlinkInstallError extends Error {}
28
28
 
29
+ function verboseEnabled(options = {}) {
30
+ if (options.verbose === true) return true;
31
+ return ['1', 'true', 'yes', 'on'].includes(
32
+ String(process.env.VLINK_VERBOSE || '').toLowerCase()
33
+ );
34
+ }
35
+
36
+ function verboseLog(message, options = {}) {
37
+ if (!verboseEnabled(options)) return;
38
+ (options.output || process.stderr).write(`[vlink:verbose] ${message}\n`);
39
+ }
40
+
41
+ function displayUrl(value) {
42
+ try {
43
+ const url = new URL(value);
44
+ url.username = '';
45
+ url.password = '';
46
+ url.search = '';
47
+ url.hash = '';
48
+ return url.toString();
49
+ } catch {
50
+ return '<invalid-url>';
51
+ }
52
+ }
53
+
29
54
  function parseGlibcVersion(text) {
30
55
  for (const line of String(text || '').split(/\r?\n/)) {
31
56
  const glibcMatch = line.match(/\bglibc\s+([0-9]+(?:\.[0-9]+)+)/);
@@ -137,7 +162,7 @@ function httpGetText(url) {
137
162
  if (res.statusCode !== 200) {
138
163
  res.resume();
139
164
  return reject(
140
- new VlinkInstallError(`HTTP ${res.statusCode} from ${target}`)
165
+ new VlinkInstallError(`HTTP ${res.statusCode} from ${displayUrl(target)}`)
141
166
  );
142
167
  }
143
168
 
@@ -308,7 +333,9 @@ function downloadFile(url, destPath) {
308
333
  if (res.statusCode !== 200) {
309
334
  res.resume();
310
335
  return reject(
311
- new VlinkInstallError(`download failed: HTTP ${res.statusCode} from ${target}`)
336
+ new VlinkInstallError(
337
+ `download failed: HTTP ${res.statusCode} from ${displayUrl(target)}`
338
+ )
312
339
  );
313
340
  }
314
341
  res.pipe(fileStream);
@@ -339,14 +366,23 @@ function verifyBinary(binaryPath) {
339
366
  }
340
367
  }
341
368
 
342
- async function ensureBinary({ force = false } = {}) {
369
+ async function ensureBinary(options = {}) {
370
+ const force = options.force === true;
343
371
  const spec = platformSpec();
344
372
  const binaryPath = resolveBinaryPath(spec);
373
+ verboseLog(
374
+ `binary resolver: version=${VERSION} platform=${spec.platform}/${spec.arch} ` +
375
+ `suffixes=${spec.suffixes.join(',')}`,
376
+ options
377
+ );
378
+ verboseLog(`binary resolver: selected path=${binaryPath} force=${force}`, options);
345
379
  if (!force && isExecutable(binaryPath)) {
380
+ verboseLog('binary resolver: using existing executable', options);
346
381
  return binaryPath;
347
382
  }
348
383
 
349
384
  const dir = path.dirname(binaryPath);
385
+ verboseLog(`binary resolver: creating download directory ${dir}`, options);
350
386
  fs.mkdirSync(dir, { recursive: true });
351
387
 
352
388
  let lastError = new VlinkInstallError('no download source available');
@@ -354,15 +390,22 @@ async function ensureBinary({ force = false } = {}) {
354
390
  for (const suffix of spec.suffixes) {
355
391
  const tempPath = path.join(dir, `.${spec.binaryName}.download-${process.pid}`);
356
392
  const sources = buildDownloadSources(spec, suffix);
393
+ verboseLog(
394
+ `download stage: suffix=${suffix} sources=${sources.map((source) => source.name).join(',')}`,
395
+ options
396
+ );
357
397
  for (const source of sources) {
358
398
  try {
399
+ verboseLog(`download stage: resolving ${source.name}`, options);
359
400
  const url = await source.resolve();
360
- console.error(`[vlink] downloading ${url} [${source.name}]`);
401
+ console.error(`[vlink] downloading ${displayUrl(url)} [${source.name}]`);
361
402
  fs.rmSync(tempPath, { force: true });
362
403
  await downloadFile(url, tempPath);
404
+ verboseLog(`download stage: received ${source.name} payload`, options);
363
405
 
364
406
  const checksum = expectedChecksum(spec, suffix);
365
407
  if (checksum) {
408
+ verboseLog(`verification stage: checking SHA-256 for ${suffix}`, options);
366
409
  const actual = await sha256File(tempPath);
367
410
  if (actual !== checksum) {
368
411
  throw new VlinkInstallError(
@@ -376,7 +419,9 @@ async function ensureBinary({ force = false } = {}) {
376
419
  }
377
420
  if (fs.existsSync(binaryPath)) fs.rmSync(binaryPath, { force: true });
378
421
  fs.renameSync(tempPath, binaryPath);
422
+ verboseLog(`verification stage: executing ${binaryPath} -V`, options);
379
423
  verifyBinary(binaryPath);
424
+ verboseLog('verification stage: binary is ready', options);
380
425
  return binaryPath;
381
426
  } catch (err) {
382
427
  lastError = err;
@@ -393,6 +438,8 @@ module.exports = {
393
438
  VlinkInstallError,
394
439
  VERSION,
395
440
  GLIBC217_SUFFIX,
441
+ verboseEnabled,
442
+ displayUrl,
396
443
  parseGlibcVersion,
397
444
  glibcVersion,
398
445
  versionAtLeast,
package/installer.js CHANGED
@@ -8,6 +8,8 @@ const { spawnSync } = require('child_process');
8
8
 
9
9
  const installModule = require('./install');
10
10
 
11
+ const PERMISSION_ERRORS = new Set(['EACCES', 'EPERM', 'EROFS']);
12
+
11
13
  const CURRENT_WRAPPER = path.resolve(__dirname, 'bin', 'vlink.js');
12
14
 
13
15
  class VlinkSetupError extends Error {}
@@ -16,6 +18,20 @@ function log(message, output = process.stderr) {
16
18
  output.write(`[TedBrain] ${message}\n`);
17
19
  }
18
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
+
19
35
  function environmentValue(env, name) {
20
36
  const expected = name.toLowerCase();
21
37
  for (const key of Object.keys(env)) {
@@ -110,8 +126,8 @@ function prependEnvironmentPath(directory, env, platform) {
110
126
  const entries = String(current)
111
127
  .split(delimiter)
112
128
  .filter(Boolean);
113
- const alreadyPresent = entries.some((entry) => entry === directory);
114
- const next = alreadyPresent ? entries.join(delimiter) : [directory, ...entries].join(delimiter);
129
+ const next = [directory, ...entries.filter((entry) => !samePath(entry, directory, platform))]
130
+ .join(delimiter);
115
131
 
116
132
  for (const key of Object.keys(env)) {
117
133
  if (key.toLowerCase() === 'path') delete env[key];
@@ -137,8 +153,9 @@ function updateWindowsUserPath(directory, options) {
137
153
  "$entry = $env:TEDBRAIN_INSTALL_DIR.TrimEnd('\\')",
138
154
  "$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')",
139
155
  "$entries = @($userPath -split ';' | Where-Object { $_ })",
140
- "if (-not ($entries | Where-Object { $_.TrimEnd('\\') -ieq $entry })) {",
141
- " $newPath = (@($env:TEDBRAIN_INSTALL_DIR) + $entries) -join ';'",
156
+ "$remaining = @($entries | Where-Object { $_.TrimEnd('\\') -ine $entry })",
157
+ "$newPath = (@($env:TEDBRAIN_INSTALL_DIR) + $remaining) -join ';'",
158
+ "if ($newPath -cne $userPath) {",
142
159
  " [Environment]::SetEnvironmentVariable('Path', $newPath, 'User')",
143
160
  " Write-Output 'changed'",
144
161
  "}"
@@ -167,6 +184,7 @@ function ensureUserPath(installDir, options = {}) {
167
184
  const fsImpl = options.fs || fs;
168
185
  const output = options.output || process.stderr;
169
186
 
187
+ verboseLog(`PATH stage: prioritizing ${installDir}`, options);
170
188
  prependEnvironmentPath(installDir, env, platform);
171
189
 
172
190
  if (platform === 'win32') {
@@ -288,12 +306,16 @@ function installBinary(sourcePath, targetPath, options = {}) {
288
306
  const spawnSyncImpl = options.spawnSync || spawnSync;
289
307
  const output = options.output || process.stderr;
290
308
 
291
- 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);
292
314
  fsImpl.mkdirSync(path.dirname(targetPath), { recursive: true });
293
315
 
294
316
  if (options.stopDaemon) {
295
317
  log('stopping the existing vlink daemon before replacement', output);
296
- const stop = spawnSyncImpl(sourcePath, ['daemon', 'stop'], { stdio: 'inherit' });
318
+ const stop = spawnSyncImpl(sourcePath, ['daemon', 'stop'], { stdio: 'inherit', env: options.env });
297
319
  const failure = commandFailure(stop, 'vlink daemon stop');
298
320
  if (failure) throw new VlinkSetupError(failure);
299
321
  }
@@ -311,7 +333,9 @@ function installBinary(sourcePath, targetPath, options = {}) {
311
333
  fsImpl.renameSync(temporary, targetPath);
312
334
  } catch (error) {
313
335
  fsImpl.rmSync(temporary, { force: true });
314
- throw new VlinkSetupError(`failed to install vlink at ${targetPath}: ${error.message}`);
336
+ const failure = new VlinkSetupError(`failed to install vlink at ${targetPath}: ${error.message}`);
337
+ failure.code = error.code;
338
+ throw failure;
315
339
  }
316
340
  fsImpl.rmSync(temporary, { force: true });
317
341
  log(`installed vlink at ${targetPath}`, output);
@@ -360,10 +384,12 @@ function installWindowsCommandShim(vlinkPath, options = {}) {
360
384
  function installShellCompletion(vlinkPath, options = {}) {
361
385
  const spawnSyncImpl = options.spawnSync || spawnSync;
362
386
  const output = options.output || process.stderr;
387
+ verboseLog(`completion stage: executing ${vlinkPath}`, options);
363
388
  const result = spawnSyncImpl(
364
389
  vlinkPath,
365
390
  ['completion', '--install', '--quiet'],
366
391
  {
392
+ env: options.env,
367
393
  encoding: 'utf8',
368
394
  stdio: ['ignore', 'pipe', 'pipe']
369
395
  }
@@ -394,7 +420,11 @@ async function migrateStaticVlink(vlinkPath, spec, options) {
394
420
  if (!binaryIsStaticElf(vlinkPath, options)) return vlinkPath;
395
421
 
396
422
  log('migrating the installed static vlink to the glibc 2.17 build', options.output);
397
- 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
+ });
398
428
  return installBinary(sourcePath, vlinkPath, {
399
429
  ...options,
400
430
  stopDaemon: true
@@ -406,6 +436,7 @@ async function runInstaller(options = {}) {
406
436
  const platform = options.platform || env.VLINK_PLATFORM || process.platform;
407
437
  const arch = options.arch || env.VLINK_ARCH || process.arch;
408
438
  const skipSetup = Boolean(options.skipSetup);
439
+ const verbose = verboseEnabled(options);
409
440
  const home = options.home || os.homedir();
410
441
  const fsImpl = options.fs || fs;
411
442
  const output = options.output || process.stderr;
@@ -414,6 +445,17 @@ async function runInstaller(options = {}) {
414
445
  const ensureBinary =
415
446
  options.ensureBinary || ((settings) => installModule.ensureBinary(settings));
416
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
+
417
459
  const spec = platformSpec({
418
460
  platform,
419
461
  arch,
@@ -423,7 +465,7 @@ async function runInstaller(options = {}) {
423
465
  : env.VLINK_GLIBC_VERSION,
424
466
  spawnSync: spawnSyncImpl
425
467
  });
426
- const installDir = defaultInstallDir(spec, env, home);
468
+ const installDir = path.resolve(defaultInstallDir(spec, env, home));
427
469
  const targetPath = installPathFor(spec, installDir);
428
470
  const sharedOptions = {
429
471
  env,
@@ -431,14 +473,32 @@ async function runInstaller(options = {}) {
431
473
  home,
432
474
  fs: fsImpl,
433
475
  output,
434
- spawnSync: spawnSyncImpl
476
+ spawnSync: spawnSyncImpl,
477
+ verbose
435
478
  };
436
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
+
437
493
  let vlinkPath = findOnPath(spec.binaryName, {
438
494
  env,
439
495
  platform,
440
496
  fs: fsImpl
441
497
  });
498
+ verboseLog(
499
+ `discovery stage: native PATH binary=${vlinkPath || 'none'}`,
500
+ sharedOptions
501
+ );
442
502
 
443
503
  if (vlinkPath) {
444
504
  log(
@@ -447,7 +507,6 @@ async function runInstaller(options = {}) {
447
507
  );
448
508
  } else if (isExecutableFile(targetPath, platform, fsImpl)) {
449
509
  vlinkPath = targetPath;
450
- ensureUserPath(installDir, sharedOptions);
451
510
  log(
452
511
  `found vlink at ${vlinkPath}; installing vlink ${installModule.VERSION}`,
453
512
  output
@@ -457,16 +516,39 @@ async function runInstaller(options = {}) {
457
516
  }
458
517
 
459
518
  const replacingExisting = Boolean(vlinkPath);
460
- const installTarget = vlinkPath || targetPath;
461
- const sourcePath = await ensureBinary({});
462
- installBinary(sourcePath, installTarget, {
463
- ...sharedOptions,
464
- stopDaemon: replacingExisting
465
- });
466
- if (
467
- !replacingExisting ||
468
- samePath(path.dirname(installTarget), installDir, platform)
469
- ) {
519
+ let installTarget = env.VLINK_INSTALL_DIR ? targetPath : (vlinkPath || targetPath);
520
+ // Avoid trying to replace a shared installation, even if its parent is writable.
521
+ if (vlinkPath && !env.VLINK_INSTALL_DIR && !samePath(vlinkPath, targetPath, platform)) {
522
+ verboseLog(`permission stage: checking ${vlinkPath} and its parent directory`, sharedOptions);
523
+ try {
524
+ fsImpl.accessSync(vlinkPath, fs.constants.W_OK);
525
+ fsImpl.accessSync(path.dirname(vlinkPath), fs.constants.W_OK);
526
+ verboseLog('permission stage: existing installation is replaceable', sharedOptions);
527
+ } catch (error) {
528
+ if (!PERMISSION_ERRORS.has(error.code)) throw error;
529
+ log(`cannot replace ${vlinkPath} (${error.code}); falling back to ${targetPath}`, output);
530
+ installTarget = targetPath;
531
+ }
532
+ }
533
+ installTarget = path.resolve(installTarget);
534
+ log(`installation target: ${installTarget}`, output);
535
+ verboseLog('binary stage: resolving packaged or cached binary', sharedOptions);
536
+ const sourcePath = await ensureBinary({ verbose, output });
537
+ verboseLog(`binary stage: source=${sourcePath}`, sharedOptions);
538
+ try {
539
+ installBinary(sourcePath, installTarget, {
540
+ ...sharedOptions,
541
+ stopDaemon: replacingExisting
542
+ });
543
+ } catch (error) {
544
+ // Permissions may change after the access check, or an ACL can deny rename.
545
+ if (env.VLINK_INSTALL_DIR || samePath(installTarget, targetPath, platform) ||
546
+ !PERMISSION_ERRORS.has(error.code)) throw error;
547
+ log(`cannot replace ${installTarget} (${error.code}); falling back to ${targetPath}`, output);
548
+ installTarget = targetPath;
549
+ installBinary(sourcePath, installTarget, sharedOptions);
550
+ }
551
+ if (samePath(path.dirname(installTarget), installDir, platform)) {
470
552
  ensureUserPath(installDir, sharedOptions);
471
553
  }
472
554
  vlinkPath = installTarget;
@@ -476,30 +558,40 @@ async function runInstaller(options = {}) {
476
558
  ensureBinary
477
559
  });
478
560
  const commandShimPath = installWindowsCommandShim(vlinkPath, sharedOptions);
561
+ verboseLog(
562
+ `shim stage: ${commandShimPath || 'not required on this platform'}`,
563
+ sharedOptions
564
+ );
479
565
  installShellCompletion(vlinkPath, sharedOptions);
480
566
 
481
567
  if (skipSetup) {
482
568
  log('skipping TedBrain Agent setup', output);
483
569
  } else {
484
570
  log('starting TedBrain Agent setup', output);
571
+ verboseLog(`setup stage: executing ${vlinkPath} setup tedbrain`, sharedOptions);
485
572
  const setup = spawnSyncImpl(vlinkPath, ['setup', 'tedbrain'], {
573
+ env,
486
574
  stdio: 'inherit'
487
575
  });
488
576
  const failure = commandFailure(setup, 'vlink setup tedbrain');
489
577
  if (failure) throw new VlinkSetupError(failure);
578
+ verboseLog('setup stage: completed successfully', sharedOptions);
490
579
  }
491
580
 
581
+ log(`installation complete: ${vlinkPath}`, output);
582
+ log('open a new terminal to use the updated PATH', output);
492
583
  return { installDir, vlinkPath, commandShimPath, setupSkipped: skipSetup };
493
584
  }
494
585
 
495
586
  function printInstallHelp(output = process.stdout) {
496
587
  output.write(
497
588
  [
498
- '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]',
499
590
  '',
500
591
  'Installs or updates vlink and adds it to the user PATH.',
501
592
  '',
502
- ' --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.'
503
595
  ].join('\n') + '\n'
504
596
  );
505
597
  }
@@ -511,18 +603,23 @@ async function runInstallerFromArgs(args, options = {}) {
511
603
  }
512
604
 
513
605
  let skipSetup = false;
606
+ let verbose = verboseEnabled(options);
514
607
  for (const arg of args) {
515
608
  if (arg === '--no-setup') {
516
609
  skipSetup = true;
610
+ } else if (arg === '--verbose') {
611
+ verbose = true;
517
612
  } else {
518
- 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}`);
519
615
  }
520
616
  }
521
- return runInstaller({ ...options, skipSetup });
617
+ return runInstaller({ ...options, skipSetup, verbose });
522
618
  }
523
619
 
524
620
  module.exports = {
525
621
  VlinkSetupError,
622
+ verboseEnabled,
526
623
  defaultInstallDir,
527
624
  installPathFor,
528
625
  isExecutableFile,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tedbrain/vlink",
3
- "version": "0.1.62",
3
+ "version": "0.1.64",
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": {