dsh-remote 0.8.49 → 0.8.51

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.
Files changed (131) hide show
  1. package/README.en.md +1 -0
  2. package/README.md +2 -0
  3. package/lib/client.js +11 -3
  4. package/lib/index.js +173 -27
  5. package/lib/sshconfig.js +256 -15
  6. package/node_modules/buildcheck/.eslintrc.js +5 -0
  7. package/node_modules/buildcheck/.github/workflows/ci.yml +88 -0
  8. package/node_modules/buildcheck/.github/workflows/lint.yml +27 -0
  9. package/node_modules/buildcheck/.npmignore +3 -0
  10. package/node_modules/buildcheck/LICENSE +19 -0
  11. package/node_modules/buildcheck/README.md +106 -0
  12. package/node_modules/buildcheck/deps/Find-VisualStudio.cs +250 -0
  13. package/node_modules/buildcheck/lib/findvs.js +361 -0
  14. package/node_modules/buildcheck/lib/index.js +738 -0
  15. package/node_modules/buildcheck/package.json +36 -0
  16. package/node_modules/buildcheck/test/test.js +8 -0
  17. package/node_modules/cpu-features/.eslintrc.js +5 -0
  18. package/node_modules/cpu-features/.github/workflows/ci.yml +74 -0
  19. package/node_modules/cpu-features/.github/workflows/lint.yml +27 -0
  20. package/node_modules/cpu-features/LICENSE +19 -0
  21. package/node_modules/cpu-features/README.md +59 -0
  22. package/node_modules/cpu-features/binding.gyp +16 -0
  23. package/node_modules/cpu-features/buildcheck.js +32 -0
  24. package/node_modules/cpu-features/deps/cpu_features/.clang-format +4 -0
  25. package/node_modules/cpu-features/deps/cpu_features/.dockerignore +31 -0
  26. package/node_modules/cpu-features/deps/cpu_features/.github/workflows/Dockerfile +5 -0
  27. package/node_modules/cpu-features/deps/cpu_features/.github/workflows/aarch64_linux_cmake.yml +30 -0
  28. package/node_modules/cpu-features/deps/cpu_features/.github/workflows/amd64_freebsd_cmake.yml +22 -0
  29. package/node_modules/cpu-features/deps/cpu_features/.github/workflows/amd64_linux_bazel.yml +26 -0
  30. package/node_modules/cpu-features/deps/cpu_features/.github/workflows/amd64_linux_cmake.yml +31 -0
  31. package/node_modules/cpu-features/deps/cpu_features/.github/workflows/amd64_macos_cmake.yml +43 -0
  32. package/node_modules/cpu-features/deps/cpu_features/.github/workflows/amd64_windows_cmake.yml +25 -0
  33. package/node_modules/cpu-features/deps/cpu_features/.github/workflows/arm_linux_cmake.yml +31 -0
  34. package/node_modules/cpu-features/deps/cpu_features/.github/workflows/clang_format.yml +24 -0
  35. package/node_modules/cpu-features/deps/cpu_features/.github/workflows/mips_linux_cmake.yml +30 -0
  36. package/node_modules/cpu-features/deps/cpu_features/.github/workflows/power_linux_cmake.yml +29 -0
  37. package/node_modules/cpu-features/deps/cpu_features/.github/workflows/riscv_linux_cmake.yml +28 -0
  38. package/node_modules/cpu-features/deps/cpu_features/.github/workflows/s390x_linux_cmake.yml +27 -0
  39. package/node_modules/cpu-features/deps/cpu_features/.grenrc.yml +21 -0
  40. package/node_modules/cpu-features/deps/cpu_features/BUILD.bazel +329 -0
  41. package/node_modules/cpu-features/deps/cpu_features/CMakeLists.txt +261 -0
  42. package/node_modules/cpu-features/deps/cpu_features/CONTRIBUTING.md +23 -0
  43. package/node_modules/cpu-features/deps/cpu_features/LICENSE +230 -0
  44. package/node_modules/cpu-features/deps/cpu_features/README.md +272 -0
  45. package/node_modules/cpu-features/deps/cpu_features/WORKSPACE +19 -0
  46. package/node_modules/cpu-features/deps/cpu_features/bazel/ci/README.md +5 -0
  47. package/node_modules/cpu-features/deps/cpu_features/bazel/platforms.bzl +11 -0
  48. package/node_modules/cpu-features/deps/cpu_features/cmake/CpuFeaturesConfig.cmake.in +3 -0
  49. package/node_modules/cpu-features/deps/cpu_features/cmake/CpuFeaturesNdkCompatConfig.cmake.in +3 -0
  50. package/node_modules/cpu-features/deps/cpu_features/cmake/README.md +30 -0
  51. package/node_modules/cpu-features/deps/cpu_features/cmake/ci/Makefile +252 -0
  52. package/node_modules/cpu-features/deps/cpu_features/cmake/ci/README.md +40 -0
  53. package/node_modules/cpu-features/deps/cpu_features/cmake/ci/doc/docker.dot +64 -0
  54. package/node_modules/cpu-features/deps/cpu_features/cmake/ci/doc/docker.svg +312 -0
  55. package/node_modules/cpu-features/deps/cpu_features/cmake/ci/doc/generate_image.sh +7 -0
  56. package/node_modules/cpu-features/deps/cpu_features/cmake/ci/docker/amd64/Dockerfile +48 -0
  57. package/node_modules/cpu-features/deps/cpu_features/cmake/ci/docker/toolchain/Dockerfile +34 -0
  58. package/node_modules/cpu-features/deps/cpu_features/cmake/ci/sample/CMakeLists.txt +22 -0
  59. package/node_modules/cpu-features/deps/cpu_features/cmake/ci/sample/main.cpp +11 -0
  60. package/node_modules/cpu-features/deps/cpu_features/cmake/ci/vagrant/freebsd/Vagrantfile +107 -0
  61. package/node_modules/cpu-features/deps/cpu_features/cmake/googletest.CMakeLists.txt.in +15 -0
  62. package/node_modules/cpu-features/deps/cpu_features/cpu_features.gyp +105 -0
  63. package/node_modules/cpu-features/deps/cpu_features/include/cpu_features_cache_info.h +54 -0
  64. package/node_modules/cpu-features/deps/cpu_features/include/cpu_features_macros.h +384 -0
  65. package/node_modules/cpu-features/deps/cpu_features/include/cpuinfo_aarch64.h +259 -0
  66. package/node_modules/cpu-features/deps/cpu_features/include/cpuinfo_arm.h +121 -0
  67. package/node_modules/cpu-features/deps/cpu_features/include/cpuinfo_mips.h +74 -0
  68. package/node_modules/cpu-features/deps/cpu_features/include/cpuinfo_ppc.h +149 -0
  69. package/node_modules/cpu-features/deps/cpu_features/include/cpuinfo_riscv.h +72 -0
  70. package/node_modules/cpu-features/deps/cpu_features/include/cpuinfo_s390x.h +108 -0
  71. package/node_modules/cpu-features/deps/cpu_features/include/cpuinfo_x86.h +288 -0
  72. package/node_modules/cpu-features/deps/cpu_features/include/internal/bit_utils.h +40 -0
  73. package/node_modules/cpu-features/deps/cpu_features/include/internal/cpuid_x86.h +37 -0
  74. package/node_modules/cpu-features/deps/cpu_features/include/internal/filesystem.h +39 -0
  75. package/node_modules/cpu-features/deps/cpu_features/include/internal/hwcaps.h +240 -0
  76. package/node_modules/cpu-features/deps/cpu_features/include/internal/stack_line_reader.h +49 -0
  77. package/node_modules/cpu-features/deps/cpu_features/include/internal/string_view.h +110 -0
  78. package/node_modules/cpu-features/deps/cpu_features/include/internal/windows_utils.h +70 -0
  79. package/node_modules/cpu-features/deps/cpu_features/ndk_compat/CMakeLists.txt +60 -0
  80. package/node_modules/cpu-features/deps/cpu_features/ndk_compat/README.md +4 -0
  81. package/node_modules/cpu-features/deps/cpu_features/ndk_compat/cpu-features.c +205 -0
  82. package/node_modules/cpu-features/deps/cpu_features/ndk_compat/cpu-features.h +320 -0
  83. package/node_modules/cpu-features/deps/cpu_features/ndk_compat/ndk-compat-test.c +12 -0
  84. package/node_modules/cpu-features/deps/cpu_features/patches/0001-Add-Apple-Silicon-Support.patch +456 -0
  85. package/node_modules/cpu-features/deps/cpu_features/scripts/generate_badges.d +165 -0
  86. package/node_modules/cpu-features/deps/cpu_features/scripts/make_release.sh +75 -0
  87. package/node_modules/cpu-features/deps/cpu_features/scripts/run_integration.sh +490 -0
  88. package/node_modules/cpu-features/deps/cpu_features/scripts/test_integration.sh +79 -0
  89. package/node_modules/cpu-features/deps/cpu_features/src/copy.inl +19 -0
  90. package/node_modules/cpu-features/deps/cpu_features/src/define_introspection.inl +86 -0
  91. package/node_modules/cpu-features/deps/cpu_features/src/define_introspection_and_hwcaps.inl +26 -0
  92. package/node_modules/cpu-features/deps/cpu_features/src/equals.inl +22 -0
  93. package/node_modules/cpu-features/deps/cpu_features/src/filesystem.c +62 -0
  94. package/node_modules/cpu-features/deps/cpu_features/src/hwcaps.c +169 -0
  95. package/node_modules/cpu-features/deps/cpu_features/src/impl_aarch64__base_implementation.inl +88 -0
  96. package/node_modules/cpu-features/deps/cpu_features/src/impl_aarch64_linux_or_android.c +79 -0
  97. package/node_modules/cpu-features/deps/cpu_features/src/impl_aarch64_macos_or_iphone.c +82 -0
  98. package/node_modules/cpu-features/deps/cpu_features/src/impl_aarch64_windows.c +138 -0
  99. package/node_modules/cpu-features/deps/cpu_features/src/impl_arm_linux_or_android.c +212 -0
  100. package/node_modules/cpu-features/deps/cpu_features/src/impl_mips_linux_or_android.c +93 -0
  101. package/node_modules/cpu-features/deps/cpu_features/src/impl_ppc_linux.c +163 -0
  102. package/node_modules/cpu-features/deps/cpu_features/src/impl_riscv_linux.c +111 -0
  103. package/node_modules/cpu-features/deps/cpu_features/src/impl_s390x_linux.c +120 -0
  104. package/node_modules/cpu-features/deps/cpu_features/src/impl_x86__base_implementation.inl +2055 -0
  105. package/node_modules/cpu-features/deps/cpu_features/src/impl_x86_freebsd.c +68 -0
  106. package/node_modules/cpu-features/deps/cpu_features/src/impl_x86_linux_or_android.c +58 -0
  107. package/node_modules/cpu-features/deps/cpu_features/src/impl_x86_macos.c +57 -0
  108. package/node_modules/cpu-features/deps/cpu_features/src/impl_x86_windows.c +58 -0
  109. package/node_modules/cpu-features/deps/cpu_features/src/stack_line_reader.c +132 -0
  110. package/node_modules/cpu-features/deps/cpu_features/src/string_view.c +192 -0
  111. package/node_modules/cpu-features/deps/cpu_features/src/utils/list_cpu_features.c +461 -0
  112. package/node_modules/cpu-features/deps/cpu_features/test/CMakeLists.txt +114 -0
  113. package/node_modules/cpu-features/deps/cpu_features/test/bit_utils_test.cc +53 -0
  114. package/node_modules/cpu-features/deps/cpu_features/test/cpuinfo_aarch64_test.cc +396 -0
  115. package/node_modules/cpu-features/deps/cpu_features/test/cpuinfo_arm_test.cc +384 -0
  116. package/node_modules/cpu-features/deps/cpu_features/test/cpuinfo_mips_test.cc +176 -0
  117. package/node_modules/cpu-features/deps/cpu_features/test/cpuinfo_ppc_test.cc +131 -0
  118. package/node_modules/cpu-features/deps/cpu_features/test/cpuinfo_riscv_test.cc +180 -0
  119. package/node_modules/cpu-features/deps/cpu_features/test/cpuinfo_s390x_test.cc +82 -0
  120. package/node_modules/cpu-features/deps/cpu_features/test/cpuinfo_x86_test.cc +1729 -0
  121. package/node_modules/cpu-features/deps/cpu_features/test/filesystem_for_testing.cc +103 -0
  122. package/node_modules/cpu-features/deps/cpu_features/test/filesystem_for_testing.h +61 -0
  123. package/node_modules/cpu-features/deps/cpu_features/test/hwcaps_for_testing.cc +52 -0
  124. package/node_modules/cpu-features/deps/cpu_features/test/hwcaps_for_testing.h +31 -0
  125. package/node_modules/cpu-features/deps/cpu_features/test/stack_line_reader_test.cc +132 -0
  126. package/node_modules/cpu-features/deps/cpu_features/test/string_view_test.cc +202 -0
  127. package/node_modules/cpu-features/lib/index.js +5 -0
  128. package/node_modules/cpu-features/package.json +40 -0
  129. package/node_modules/cpu-features/src/binding.cc +151 -0
  130. package/node_modules/cpu-features/test/test.js +12 -0
  131. package/package.json +1 -1
package/lib/sshconfig.js CHANGED
@@ -28,7 +28,7 @@
28
28
  // The module is pure (I/O is injectable), so it is unit-testable without a real
29
29
  // home directory — see test/sshconfig.test.js.
30
30
 
31
- import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'
31
+ import { existsSync, readFileSync, readdirSync, statSync, mkdirSync, writeFileSync, chmodSync } from 'node:fs'
32
32
  import { homedir } from 'node:os'
33
33
  import path from 'node:path'
34
34
 
@@ -362,23 +362,14 @@ export function parseProxyJump(value) {
362
362
  }
363
363
 
364
364
  /**
365
- * Connect target for an ssh-config alias: the shape the plugin's pool expects.
365
+ * Shared proxy/warning shaping for alias and literal resolutions (issue #50).
366
366
  *
367
367
  * `ProxyJump` maps onto the plugin's single jump host. A multi-hop chain cannot
368
368
  * be represented by one `proxy`, so the FIRST hop is used and `warnings` says so
369
369
  * — silently connecting through the wrong path would be worse. `ProxyCommand`
370
370
  * (an arbitrary local command) is reported but never executed.
371
- *
372
- * @param {string} alias
373
- * @param {object} [opts]
374
- * @param {string} [opts.text] - config text (else the real file is read).
375
- * @param {string} [opts.file]
376
- * @param {object} [opts.io]
377
- * @returns {{alias: string, matched: boolean, host: string, port: number, username: string, privateKeyPath: string, proxy: object|undefined, proxyJump: string, warnings: string[]}}
378
371
  */
379
- export function sshAliasTarget(alias, opts = {}) {
380
- const text = opts.text !== undefined ? String(opts.text) : loadSshConfigText(opts.file || sshConfigPath(), opts.io || {})
381
- const resolved = resolveSshHost(alias, text)
372
+ function targetFromResolved(resolved, text, { defaultPort }) {
382
373
  const warnings = []
383
374
  let proxy
384
375
  const hops = parseProxyJump(resolved.proxyJump)
@@ -400,10 +391,10 @@ export function sshAliasTarget(alias, opts = {}) {
400
391
  warnings.push('ProxyCommand is not supported by dsh-remote (no jump host was applied).')
401
392
  }
402
393
  return {
403
- alias,
394
+ alias: resolved.alias,
404
395
  matched: resolved.matched,
405
- host: resolved.hostName || alias,
406
- port: resolved.port || 22,
396
+ host: resolved.hostName || resolved.alias,
397
+ port: resolved.port || defaultPort,
407
398
  username: resolved.user || '',
408
399
  privateKeyPath: resolved.identityFile ? expandHome(resolved.identityFile) : '',
409
400
  proxy,
@@ -412,6 +403,72 @@ export function sshAliasTarget(alias, opts = {}) {
412
403
  }
413
404
  }
414
405
 
406
+ /**
407
+ * Connect target for an ssh-config alias: the shape the plugin's pool expects.
408
+ *
409
+ * @param {string} alias
410
+ * @param {object} [opts]
411
+ * @param {string} [opts.text] - config text (else the real file is read).
412
+ * @param {string} [opts.file]
413
+ * @param {object} [opts.io]
414
+ * @returns {{alias: string, matched: boolean, host: string, port: number, username: string, privateKeyPath: string, proxy: object|undefined, proxyJump: string, warnings: string[]}}
415
+ */
416
+ export function sshAliasTarget(alias, opts = {}) {
417
+ const text = opts.text !== undefined ? String(opts.text) : loadSshConfigText(opts.file || sshConfigPath(), opts.io || {})
418
+ return targetFromResolved(resolveSshHost(alias, text), text, { defaultPort: 22 })
419
+ }
420
+
421
+ /**
422
+ * Effective connect identity for a LITERAL machine (issue #50).
423
+ *
424
+ * A machine saved with literal host/user/port/key holds a COPY of what may also
425
+ * be written in ~/.ssh/config, and the copy goes stale the moment the file is
426
+ * edited. The file is the single source for any host it NAMES: when a Host
427
+ * block matches the machine's literal host — with at least one positive pattern
428
+ * that is not the bare `*` — the resolved values win per field and the record
429
+ * fills the gaps. A bare `Host *` block deliberately does NOT name a host:
430
+ * treating universal defaults as an override would hijack every saved machine
431
+ * at once. Group patterns (`*.corp`, `web-?`) DO name hosts, exactly as ssh
432
+ * itself applies them to a literal dial.
433
+ *
434
+ * `port` is 0 (not 22) when the file says nothing, so the caller falls back to
435
+ * the record instead of the default.
436
+ *
437
+ * @param {object} machine - a registry record with a literal `host`.
438
+ * @param {{text?: string, file?: string, io?: object}} [opts]
439
+ * @returns {null | {alias: string, matched: true, host: string, port: number, username: string, privateKeyPath: string, proxy: object|undefined, proxyJump: string, warnings: string[]}}
440
+ * null when no Host block names the machine's host.
441
+ */
442
+ export function resolveLiteralSshConfig(machine, opts = {}) {
443
+ if (!machine || machine.useSshConfig) return null
444
+ const name = String(machine.host || '').trim()
445
+ if (!name) return null
446
+ const text = opts.text !== undefined ? String(opts.text) : loadSshConfigText(opts.file || sshConfigPath(), opts.io || {})
447
+ const resolved = {
448
+ alias: name, matched: false, hostName: '', user: '', port: 0,
449
+ identityFile: '', proxyJump: '', proxyCommand: '', identitiesOnly: '',
450
+ }
451
+ for (const block of parseHostBlocks(text)) {
452
+ if (!hostBlockMatches(block.patterns, name)) continue
453
+ const positive = block.patterns.filter((p) => !p.startsWith('!'))
454
+ // A block whose only positive pattern is the bare `*` is a defaults block,
455
+ // not a block that names this host.
456
+ if (positive.length && positive.every((p) => p === '*')) continue
457
+ resolved.matched = true
458
+ for (const [key, value] of Object.entries(block.values)) {
459
+ if (key === 'hostname') { if (!resolved.hostName) resolved.hostName = String(value) }
460
+ else if (key === 'user') { if (!resolved.user) resolved.user = String(value) }
461
+ else if (key === 'port') { if (!resolved.port) resolved.port = Number(value) || 0 }
462
+ else if (key === 'identityfile') { if (!resolved.identityFile) resolved.identityFile = String(value) }
463
+ else if (key === 'proxyjump') { if (!resolved.proxyJump) resolved.proxyJump = String(value) }
464
+ else if (key === 'proxycommand') { if (!resolved.proxyCommand) resolved.proxyCommand = String(value) }
465
+ else if (key === 'identitiesonly') { if (!resolved.identitiesOnly) resolved.identitiesOnly = String(value) }
466
+ }
467
+ }
468
+ if (!resolved.matched) return null
469
+ return targetFromResolved(resolved, text, { defaultPort: 0 })
470
+ }
471
+
415
472
  /**
416
473
  * Effective connect identity for a saved machine.
417
474
  *
@@ -452,3 +509,187 @@ export function resolveMachineSshConfig(machine, opts = {}) {
452
509
  function compareText(left, right) {
453
510
  return left < right ? -1 : left > right ? 1 : 0
454
511
  }
512
+
513
+ // ── Writing: the managed Host block (issue #50) ─────────────────────────────
514
+ //
515
+ // The four connection fields (host/user/port/key) have ONE home: ~/.ssh/config.
516
+ // When the plugin saves a literal machine it upserts a Host block tagged with
517
+ // MANAGED_MARK, so the file and the registry can never drift — and so the
518
+ // plugin can tell ITS OWN block apart from one the user wrote. Two rules keep
519
+ // the file safe:
520
+ // • a Host block without the marker is never touched — a same-named user
521
+ // block is a CONFLICT, reported to the caller instead of overwritten;
522
+ // • inside a managed block, lines the plugin does not own (a user-added
523
+ // `ForwardAgent yes`, a comment) survive an update — only the four known
524
+ // parameters are rewritten.
525
+
526
+ /** Marker comment that tags a Host block as owned by dsh-remote. */
527
+ export const MANAGED_MARK = '# dsh-remote: managed'
528
+
529
+ /** Parameters the managed block rewrites (lowercase). */
530
+ const MANAGED_PARAMS = new Set(['hostname', 'user', 'port', 'identityfile'])
531
+
532
+ /** Read the RAW config text — no Include expansion (a write targets ONE file). */
533
+ export function readRawSshConfig(filePath = sshConfigPath()) {
534
+ try {
535
+ const text = readFileSync(filePath, 'utf8')
536
+ return typeof text === 'string' ? text : ''
537
+ } catch {
538
+ return ''
539
+ }
540
+ }
541
+
542
+ /** Quote one value for a config line when it contains whitespace. */
543
+ function quoteCfg(value) {
544
+ const s = String(value)
545
+ return /[\s#]/.test(s) ? `"${s.replace(/"/g, '\\"')}"` : s
546
+ }
547
+
548
+ /** The values a managed block must carry for these fields ('' = omit). */
549
+ function desiredValues(fields) {
550
+ const alias = String(fields.alias || '').trim()
551
+ const hostName = String(fields.hostName || '').trim()
552
+ const port = Number(fields.port) || 0
553
+ return {
554
+ hostname: hostName && hostName !== alias ? hostName : '',
555
+ user: String(fields.user || '').trim(),
556
+ port: port && port !== 22 ? port : '',
557
+ identityfile: String(fields.identityFile || '').trim(),
558
+ }
559
+ }
560
+
561
+ /** Render the owned body lines of a managed block. */
562
+ function desiredLines(fields) {
563
+ const want = desiredValues(fields)
564
+ const out = []
565
+ if (want.hostname) out.push(` HostName ${quoteCfg(want.hostname)}`)
566
+ if (want.user) out.push(` User ${quoteCfg(want.user)}`)
567
+ if (want.port) out.push(` Port ${want.port}`)
568
+ if (want.identityfile) out.push(` IdentityFile ${quoteCfg(want.identityfile)}`)
569
+ return out
570
+ }
571
+
572
+ /**
573
+ * Locate the MANAGED Host block for `alias` in raw config lines.
574
+ * The marker must be the nearest non-blank line above the Host line; a block
575
+ * that names the alias without the marker is the user's, not ours.
576
+ */
577
+ function locateManagedBlock(lines, alias) {
578
+ const wanted = alias.toLowerCase()
579
+ for (let i = 0; i < lines.length; i += 1) {
580
+ const m = lines[i].match(/^\s*host\s+(.+?)\s*$/i)
581
+ if (!m) continue
582
+ const names = splitValue(m[1]).map((p) => p.toLowerCase())
583
+ if (!names.includes(wanted)) continue
584
+ let j = i - 1
585
+ while (j >= 0 && lines[j].trim() === '') j -= 1
586
+ if (j < 0 || lines[j].trim().toLowerCase() !== MANAGED_MARK) continue
587
+ let end = lines.length
588
+ for (let k = i + 1; k < lines.length; k += 1) {
589
+ if (/^\s*(host|match)\s/i.test(lines[k])) { end = k; break }
590
+ }
591
+ return { markerLine: j, hostLine: i, end }
592
+ }
593
+ return null
594
+ }
595
+
596
+ /** Whether an UNMANAGED Host block names `alias` (exact token, case-insensitive). */
597
+ function hasUnmanagedConflict(lines, alias) {
598
+ const wanted = alias.toLowerCase()
599
+ for (let i = 0; i < lines.length; i += 1) {
600
+ const m = lines[i].match(/^\s*host\s+(.+?)\s*$/i)
601
+ if (!m) continue
602
+ const names = splitValue(m[1]).map((p) => p.toLowerCase())
603
+ if (!names.includes(wanted)) continue
604
+ let j = i - 1
605
+ while (j >= 0 && lines[j].trim() === '') j -= 1
606
+ if (j >= 0 && lines[j].trim().toLowerCase() === MANAGED_MARK) continue // ours
607
+ return true
608
+ }
609
+ return false
610
+ }
611
+
612
+ /**
613
+ * Insert or refresh the managed Host block for a saved machine.
614
+ *
615
+ * @param {string} rawText - the RAW ~/.ssh/config text ('' when absent).
616
+ * @param {{alias: string, hostName?: string, user?: string, port?: number|string, identityFile?: string}} fields
617
+ * @returns {{status: 'appended'|'updated'|'unchanged'|'conflict'|'invalid', text: string}}
618
+ * 'conflict' — an unmanaged block already names the alias (text untouched);
619
+ * 'invalid' — the alias can never be a safe Host token (space/wildcard).
620
+ */
621
+ export function upsertManagedHostBlock(rawText, fields) {
622
+ const text = String(rawText || '')
623
+ const alias = String((fields && fields.alias) || '').trim()
624
+ if (!alias || /[\s*?!,]/.test(alias)) return { status: 'invalid', text }
625
+ const lines = text.split('\n')
626
+ if (hasUnmanagedConflict(lines, alias)) return { status: 'conflict', text }
627
+
628
+ const want = desiredValues({ ...fields, alias })
629
+ const found = locateManagedBlock(lines, alias)
630
+ if (!found) {
631
+ const block = [MANAGED_MARK, `Host ${alias}`, ...desiredLines({ ...fields, alias })]
632
+ const trimmedEnd = text.replace(/\s+$/, '')
633
+ const prefix = trimmedEnd === '' ? '' : trimmedEnd + '\n\n'
634
+ return { status: 'appended', text: prefix + block.join('\n') + '\n' }
635
+ }
636
+
637
+ // Compare what the block HAS with what it MUST have; a rewrite that changes
638
+ // nothing would only cost the file's mtime.
639
+ const current = parseHostBlocks(lines.slice(found.hostLine, found.end).join('\n'))[0]
640
+ const cur = (current && current.values) || {}
641
+ const same = ['hostname', 'user', 'port', 'identityfile']
642
+ .every((k) => String(cur[k] ?? '') === String(want[k] ?? ''))
643
+ if (same) return { status: 'unchanged', text }
644
+
645
+ // Rewrite only the four owned parameters; user lines inside OUR block stay.
646
+ const body = lines.slice(found.hostLine + 1, found.end)
647
+ const kept = body.filter((l) => {
648
+ const m = l.match(/^\s*(\S+)\s/)
649
+ return !m || !MANAGED_PARAMS.has(m[1].toLowerCase())
650
+ })
651
+ const next = [
652
+ ...lines.slice(0, found.hostLine + 1),
653
+ ...desiredLines({ ...fields, alias }),
654
+ ...kept,
655
+ ...lines.slice(found.end),
656
+ ]
657
+ return { status: 'updated', text: next.join('\n') }
658
+ }
659
+
660
+ /**
661
+ * Remove the managed block for `alias` (used when a machine is RENAMED away:
662
+ * the old block is the plugin's own artifact of that edit). Deleting a machine
663
+ * deliberately does NOT call this — the Host block is the user's ssh access,
664
+ * not plugin state. Unmanaged blocks are never removed.
665
+ *
666
+ * @returns {{status: 'removed'|'absent', text: string}}
667
+ */
668
+ export function removeManagedHostBlock(rawText, alias) {
669
+ const text = String(rawText || '')
670
+ const name = String(alias || '').trim()
671
+ if (!name) return { status: 'absent', text }
672
+ const lines = text.split('\n')
673
+ const found = locateManagedBlock(lines, name)
674
+ if (!found) return { status: 'absent', text }
675
+ let start = found.markerLine
676
+ while (start > 0 && lines[start - 1].trim() === '') start -= 1
677
+ const next = [...lines.slice(0, start), ...lines.slice(found.end)]
678
+ return { status: 'removed', text: next.join('\n') }
679
+ }
680
+
681
+ /**
682
+ * Persist a config text, creating ~/.ssh first. Permissions follow OpenSSH's
683
+ * expectations on POSIX (dir 0700, file 0600) and are a no-op elsewhere.
684
+ */
685
+ export function writeSshConfig(text, filePath = sshConfigPath(), io = {}) {
686
+ const mkdir = io.mkdir || ((p) => mkdirSync(p, { recursive: true }))
687
+ const write = io.write || ((p, data) => writeFileSync(p, data, 'utf8'))
688
+ const chmod = io.chmod || ((p, mode) => { try { chmodSync(p, mode) } catch { /* no modes here */ } })
689
+ mkdir(path.dirname(filePath))
690
+ write(filePath, String(text))
691
+ if (process.platform !== 'win32') {
692
+ chmod(path.dirname(filePath), 0o700)
693
+ chmod(filePath, 0o600)
694
+ }
695
+ }
@@ -0,0 +1,5 @@
1
+ 'use strict';
2
+
3
+ module.exports = {
4
+ extends: '@mscdex/eslint-config',
5
+ };
@@ -0,0 +1,88 @@
1
+ name: CI
2
+
3
+ on:
4
+ pull_request:
5
+ push:
6
+ branches: [ master ]
7
+
8
+ jobs:
9
+ tests-linux:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ fail-fast: false
13
+ matrix:
14
+ node-version: [10.x, 12.x, 14.x, 16.x, 18.x, 20.x, 22.x, 24.x]
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ with:
18
+ persist-credentials: false
19
+ - name: Use Node.js ${{ matrix.node-version }}
20
+ uses: actions/setup-node@v4
21
+ with:
22
+ node-version: ${{ matrix.node-version }}
23
+ - name: Check Node.js version
24
+ run: node -pe process.versions
25
+ - name: Install module
26
+ run: npm install
27
+ - name: Run tests
28
+ run: npm test
29
+ tests-macos-intel:
30
+ runs-on: macos-15-intel
31
+ strategy:
32
+ fail-fast: false
33
+ matrix:
34
+ node-version: [10.x, 12.x, 14.x, 16.x, 18.x, 20.x, 22.x, 24.x]
35
+ steps:
36
+ - uses: actions/checkout@v4
37
+ with:
38
+ persist-credentials: false
39
+ - name: Use Node.js ${{ matrix.node-version }}
40
+ uses: actions/setup-node@v4
41
+ with:
42
+ node-version: ${{ matrix.node-version }}
43
+ - name: Check Node.js version
44
+ run: node -pe process.versions
45
+ - name: Install module
46
+ run: npm install
47
+ - name: Run tests
48
+ run: npm test
49
+ tests-macos-arm:
50
+ runs-on: macos-latest
51
+ strategy:
52
+ fail-fast: false
53
+ matrix:
54
+ node-version: [16.x, 18.x, 20.x, 22.x, 24.x]
55
+ steps:
56
+ - uses: actions/checkout@v4
57
+ with:
58
+ persist-credentials: false
59
+ - name: Use Node.js ${{ matrix.node-version }}
60
+ uses: actions/setup-node@v4
61
+ with:
62
+ node-version: ${{ matrix.node-version }}
63
+ - name: Check Node.js version
64
+ run: node -pe process.versions
65
+ - name: Install module
66
+ run: npm install
67
+ - name: Run tests
68
+ run: npm test
69
+ tests-windows-2022:
70
+ runs-on: windows-2022
71
+ strategy:
72
+ fail-fast: false
73
+ matrix:
74
+ node-version: [24.x]
75
+ steps:
76
+ - uses: actions/checkout@v4
77
+ with:
78
+ persist-credentials: false
79
+ - name: Use Node.js ${{ matrix.node-version }}
80
+ uses: actions/setup-node@v4
81
+ with:
82
+ node-version: ${{ matrix.node-version }}
83
+ - name: Check Node.js version
84
+ run: node -pe process.versions
85
+ - name: Install module
86
+ run: npm install
87
+ - name: Run tests
88
+ run: npm test
@@ -0,0 +1,27 @@
1
+ name: lint
2
+
3
+ on:
4
+ pull_request:
5
+ push:
6
+ branches: [ master ]
7
+
8
+ env:
9
+ NODE_VERSION: 22.x
10
+
11
+ jobs:
12
+ lint-js:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ with:
17
+ persist-credentials: false
18
+ - name: Use Node.js ${{ env.NODE_VERSION }}
19
+ uses: actions/setup-node@v4
20
+ with:
21
+ node-version: ${{ env.NODE_VERSION }}
22
+ - name: Check Node.js version
23
+ run: node -pe process.versions
24
+ - name: Install ESLint + ESLint configs/plugins
25
+ run: npm install --only=dev
26
+ - name: Lint files
27
+ run: npm run lint
@@ -0,0 +1,3 @@
1
+ .eslintcache
2
+ **/node_modules/**
3
+ /package-lock.json
@@ -0,0 +1,19 @@
1
+ Copyright Brian White. All rights reserved.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to
5
+ deal in the Software without restriction, including without limitation the
6
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
7
+ sell copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
19
+ IN THE SOFTWARE.
@@ -0,0 +1,106 @@
1
+ # Description
2
+
3
+ Build environment checking for [node.js](http://nodejs.org/).
4
+
5
+ This allows for autoconf-like functionality for node addons/build scripts.
6
+
7
+ **Note:** Obsolete and/or exotic build environments or platforms not supported
8
+ by node.js are not supported.
9
+
10
+ ## Requirements
11
+
12
+ * [node.js](http://nodejs.org/) -- v10.0.0 or newer
13
+ * Supported compilers:
14
+ * gcc
15
+ * clang
16
+ * MSVC 2013+ and Windows SDK 8.1+
17
+
18
+ ## Installation
19
+
20
+ npm install buildcheck
21
+
22
+ ## Examples
23
+
24
+ ### Check if a C function exists
25
+
26
+ ```js
27
+ 'use strict';
28
+
29
+ const BuildEnvironment = require('buildcheck');
30
+
31
+ const buildEnv = new BuildEnvironment();
32
+
33
+ console.log(buildEnv.checkFunction('c', 'preadv2'));
34
+ ```
35
+
36
+ ### Check if a C header is usable
37
+
38
+ ```js
39
+ 'use strict';
40
+
41
+ const BuildEnvironment = require('buildcheck');
42
+
43
+ const buildEnv = new BuildEnvironment();
44
+
45
+ console.log(buildEnv.checkHeader('c', 'linux/io_uring.h'));
46
+ ```
47
+
48
+ ### Try to compile some C code
49
+
50
+ ```js
51
+ 'use strict';
52
+
53
+ const BuildEnvironment = require('buildcheck');
54
+
55
+ const buildEnv = new BuildEnvironment();
56
+
57
+ // Should be a successful compile
58
+ console.log(buildEnv.tryCompile('c', 'int main() { return 0; }'));
59
+
60
+ // Should be a failed compile
61
+ console.log(buildEnv.tryCompile('c', 'int main() { return z; }'));
62
+ ```
63
+
64
+ ## API
65
+
66
+ ### Exports
67
+
68
+ The exported value is `BuildEnvironment`, the main class for dealing with a build environment.
69
+
70
+ ### BuildEnvironment
71
+
72
+ #### Methods
73
+
74
+ * **(constructor)**([< _object_ >config]) - Creates and returns a new BuildEnvironment instance. `config` may contain:
75
+
76
+ * **compilerC** - _string_ - C compiler command to use. *Note: this is ignored on Windows.* **Default:** `process.env.CC` or `'cc'`
77
+
78
+ * **compilerCXX** - _string_ - C++ compiler command to use. *Note: this is ignored on Windows.* **Default:** `process.env.CXX` or `'c++'`
79
+
80
+ * **msvs_version** - _mixed_ - A string or number containing the year of the Visual Studio compiler to use. *Note: this is for Windows only.* **Default:** newest version installed
81
+
82
+ * **checkDeclared**(< _string_ >lang, < _string_ >symbolName[, < _object_ >options]) - _boolean_ - Checks if a symbol `symbolName` is declared where `lang` is either `'c'` or `'c++'`. Returns `true` if symbol exists, `false` otherwise. `options` may contain:
83
+
84
+ * **headers** - _array_ - A list of headers to try when checking if the symbol is declared. `checkFunction()` will always first try without a library. If not supplied, a default list of common (platform-specific) headers will be used.
85
+
86
+ * **searchLibs** - _array_ - A list of library names (without the `'-l'` prefix) to try when checking if the symbol is declared. `checkDeclared()` will always first try without a library.
87
+
88
+ * **checkFunction**(< _string_ >lang, < _string_ >functionName[, < _object_ >options]) - _boolean_ - Checks if a function `functionName` exists and is linkable where `lang` is either `'c'` or `'c++'`. Returns `true` if function exists, `false` otherwise. `options` may contain:
89
+
90
+ * **searchLibs** - _array_ - A list of library names (without the `'-l'` prefix) to try when checking for this function. `checkFunction()` will always first try without a library.
91
+
92
+ * **checkFeature**(< _string_ >featureName) - _mixed_ - Executes a special test for a "feature" and returns the result. Supported values for `featureName`:
93
+
94
+ * `'strerror_r'` - Returns an object containing:
95
+
96
+ * `declared` - _boolean_ - Whether `strerror_r()` is declared
97
+
98
+ * `returnsCharPtr` - _boolean_ - If `strerror_r()` is declared, whether it returns `char*` (a GNU extension) or not.
99
+
100
+ * **checkHeader**(< _string_ >lang, < _string_ >headerName) - _boolean_ - Checks if the header `headerName` exists and is usable where `lang` is either `'c'` or `'c++'`. Returns `true` if the header exists and is usable, `false` otherwise.
101
+
102
+ * **defines**([< _string_ >lang[, < _boolean_ >rendered]]) - _array_ - Returns a list of features, functions, headers, and symbols known to be defined by this build environment instance. `lang` is either `'c'` or `'c++'` If `lang` is not set, defines for both `'c'` and `'c++'` will be returned. If `rendered` is `true` (defaults to `false`), autoconf-style defines (e.g. "HAVE_FOO=1") will be returned instead. Defines coming from features utilize base strings/names from autoconf for better compatibility.
103
+
104
+ * **libs**([< _string_ >lang]) - _array_ - Returns a list of (`'-l'`-prefixed) libraries known to be required for features and functions defined by this build environment instance. `lang` is either `'c'` or `'c++'` If `lang` is not set, defines for both `'c'` and `'c++'` will be returned.
105
+
106
+ * **tryCompile**(< _string_ >lang, < _string_ >code[, < _array_ >compilerParams]) - _mixed_ - Attempts to compile `code` where `lang` is either `'c'` or `'c++'`. `compilerParams` is an optional array of compiler/linker flags to include. Returns `true` on successful compilation, or an _Error_ instance with an `output` property containing the compiler error output.