dsh-remote 0.8.46 → 0.8.48
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/LICENSE +21 -21
- package/README.en.md +361 -361
- package/README.md +368 -368
- package/cordis.patch.yml +9 -9
- package/lib/binding.js +172 -172
- package/lib/client.js +3637 -3637
- package/lib/credential.js +177 -177
- package/lib/deploy-tasks.js +142 -142
- package/lib/errors.js +87 -87
- package/lib/file-reference.js +457 -457
- package/lib/forwards.js +258 -258
- package/lib/hostkey.js +108 -108
- package/lib/http-transport.js +196 -196
- package/lib/ignore.js +202 -156
- package/lib/index.js +3779 -3747
- package/lib/openssh.js +838 -838
- package/lib/paths.js +225 -225
- package/lib/pool.js +757 -757
- package/lib/registry.js +95 -95
- package/lib/remote-fs.js +148 -143
- package/lib/routes-fs.js +233 -233
- package/lib/search.js +235 -230
- package/lib/sftp-surface.js +76 -76
- package/lib/sshconfig.js +454 -454
- package/lib/sync.js +275 -275
- package/lib/tasks.js +95 -95
- package/lib/telemetry.js +213 -213
- package/lib/update.js +472 -472
- package/lib/web-attach.js +850 -850
- package/lib/web-deploy-skill.js +205 -205
- package/lib/web-deploy.js +694 -694
- package/node_modules/asn1/Jenkinsfile +65 -0
- package/node_modules/asn1/LICENSE +19 -0
- package/node_modules/asn1/README.md +50 -0
- package/node_modules/asn1/lib/ber/errors.js +13 -0
- package/node_modules/asn1/lib/ber/index.js +27 -0
- package/node_modules/asn1/lib/ber/reader.js +262 -0
- package/node_modules/asn1/lib/ber/types.js +36 -0
- package/node_modules/asn1/lib/ber/writer.js +317 -0
- package/node_modules/asn1/lib/index.js +20 -0
- package/node_modules/asn1/package.json +31 -0
- package/node_modules/bcrypt-pbkdf/CONTRIBUTING.md +13 -0
- package/node_modules/bcrypt-pbkdf/LICENSE +66 -0
- package/node_modules/bcrypt-pbkdf/README.md +45 -0
- package/node_modules/bcrypt-pbkdf/index.js +556 -0
- package/node_modules/bcrypt-pbkdf/package.json +15 -0
- package/node_modules/buildcheck/.eslintrc.js +5 -0
- package/node_modules/buildcheck/.github/workflows/ci.yml +88 -0
- package/node_modules/buildcheck/.github/workflows/lint.yml +27 -0
- package/node_modules/buildcheck/.npmignore +3 -0
- package/node_modules/buildcheck/LICENSE +19 -0
- package/node_modules/buildcheck/README.md +106 -0
- package/node_modules/buildcheck/deps/Find-VisualStudio.cs +250 -0
- package/node_modules/buildcheck/lib/findvs.js +361 -0
- package/node_modules/buildcheck/lib/index.js +738 -0
- package/node_modules/buildcheck/package.json +36 -0
- package/node_modules/buildcheck/test/test.js +8 -0
- package/node_modules/cpu-features/.eslintrc.js +5 -0
- package/node_modules/cpu-features/.github/workflows/ci.yml +74 -0
- package/node_modules/cpu-features/.github/workflows/lint.yml +27 -0
- package/node_modules/cpu-features/LICENSE +19 -0
- package/node_modules/cpu-features/README.md +59 -0
- package/node_modules/cpu-features/binding.gyp +16 -0
- package/node_modules/cpu-features/buildcheck.js +32 -0
- package/node_modules/cpu-features/deps/cpu_features/.clang-format +4 -0
- package/node_modules/cpu-features/deps/cpu_features/.dockerignore +31 -0
- package/node_modules/cpu-features/deps/cpu_features/.github/workflows/Dockerfile +5 -0
- package/node_modules/cpu-features/deps/cpu_features/.github/workflows/aarch64_linux_cmake.yml +30 -0
- package/node_modules/cpu-features/deps/cpu_features/.github/workflows/amd64_freebsd_cmake.yml +22 -0
- package/node_modules/cpu-features/deps/cpu_features/.github/workflows/amd64_linux_bazel.yml +26 -0
- package/node_modules/cpu-features/deps/cpu_features/.github/workflows/amd64_linux_cmake.yml +31 -0
- package/node_modules/cpu-features/deps/cpu_features/.github/workflows/amd64_macos_cmake.yml +43 -0
- package/node_modules/cpu-features/deps/cpu_features/.github/workflows/amd64_windows_cmake.yml +25 -0
- package/node_modules/cpu-features/deps/cpu_features/.github/workflows/arm_linux_cmake.yml +31 -0
- package/node_modules/cpu-features/deps/cpu_features/.github/workflows/clang_format.yml +24 -0
- package/node_modules/cpu-features/deps/cpu_features/.github/workflows/mips_linux_cmake.yml +30 -0
- package/node_modules/cpu-features/deps/cpu_features/.github/workflows/power_linux_cmake.yml +29 -0
- package/node_modules/cpu-features/deps/cpu_features/.github/workflows/riscv_linux_cmake.yml +28 -0
- package/node_modules/cpu-features/deps/cpu_features/.github/workflows/s390x_linux_cmake.yml +27 -0
- package/node_modules/cpu-features/deps/cpu_features/.grenrc.yml +21 -0
- package/node_modules/cpu-features/deps/cpu_features/BUILD.bazel +329 -0
- package/node_modules/cpu-features/deps/cpu_features/CMakeLists.txt +261 -0
- package/node_modules/cpu-features/deps/cpu_features/CONTRIBUTING.md +23 -0
- package/node_modules/cpu-features/deps/cpu_features/LICENSE +230 -0
- package/node_modules/cpu-features/deps/cpu_features/README.md +272 -0
- package/node_modules/cpu-features/deps/cpu_features/WORKSPACE +19 -0
- package/node_modules/cpu-features/deps/cpu_features/bazel/ci/README.md +5 -0
- package/node_modules/cpu-features/deps/cpu_features/bazel/platforms.bzl +11 -0
- package/node_modules/cpu-features/deps/cpu_features/cmake/CpuFeaturesConfig.cmake.in +3 -0
- package/node_modules/cpu-features/deps/cpu_features/cmake/CpuFeaturesNdkCompatConfig.cmake.in +3 -0
- package/node_modules/cpu-features/deps/cpu_features/cmake/README.md +30 -0
- package/node_modules/cpu-features/deps/cpu_features/cmake/ci/Makefile +252 -0
- package/node_modules/cpu-features/deps/cpu_features/cmake/ci/README.md +40 -0
- package/node_modules/cpu-features/deps/cpu_features/cmake/ci/doc/docker.dot +64 -0
- package/node_modules/cpu-features/deps/cpu_features/cmake/ci/doc/docker.svg +312 -0
- package/node_modules/cpu-features/deps/cpu_features/cmake/ci/doc/generate_image.sh +7 -0
- package/node_modules/cpu-features/deps/cpu_features/cmake/ci/docker/amd64/Dockerfile +48 -0
- package/node_modules/cpu-features/deps/cpu_features/cmake/ci/docker/toolchain/Dockerfile +34 -0
- package/node_modules/cpu-features/deps/cpu_features/cmake/ci/sample/CMakeLists.txt +22 -0
- package/node_modules/cpu-features/deps/cpu_features/cmake/ci/sample/main.cpp +11 -0
- package/node_modules/cpu-features/deps/cpu_features/cmake/ci/vagrant/freebsd/Vagrantfile +107 -0
- package/node_modules/cpu-features/deps/cpu_features/cmake/googletest.CMakeLists.txt.in +15 -0
- package/node_modules/cpu-features/deps/cpu_features/cpu_features.gyp +105 -0
- package/node_modules/cpu-features/deps/cpu_features/include/cpu_features_cache_info.h +54 -0
- package/node_modules/cpu-features/deps/cpu_features/include/cpu_features_macros.h +384 -0
- package/node_modules/cpu-features/deps/cpu_features/include/cpuinfo_aarch64.h +259 -0
- package/node_modules/cpu-features/deps/cpu_features/include/cpuinfo_arm.h +121 -0
- package/node_modules/cpu-features/deps/cpu_features/include/cpuinfo_mips.h +74 -0
- package/node_modules/cpu-features/deps/cpu_features/include/cpuinfo_ppc.h +149 -0
- package/node_modules/cpu-features/deps/cpu_features/include/cpuinfo_riscv.h +72 -0
- package/node_modules/cpu-features/deps/cpu_features/include/cpuinfo_s390x.h +108 -0
- package/node_modules/cpu-features/deps/cpu_features/include/cpuinfo_x86.h +288 -0
- package/node_modules/cpu-features/deps/cpu_features/include/internal/bit_utils.h +40 -0
- package/node_modules/cpu-features/deps/cpu_features/include/internal/cpuid_x86.h +37 -0
- package/node_modules/cpu-features/deps/cpu_features/include/internal/filesystem.h +39 -0
- package/node_modules/cpu-features/deps/cpu_features/include/internal/hwcaps.h +240 -0
- package/node_modules/cpu-features/deps/cpu_features/include/internal/stack_line_reader.h +49 -0
- package/node_modules/cpu-features/deps/cpu_features/include/internal/string_view.h +110 -0
- package/node_modules/cpu-features/deps/cpu_features/include/internal/windows_utils.h +70 -0
- package/node_modules/cpu-features/deps/cpu_features/ndk_compat/CMakeLists.txt +60 -0
- package/node_modules/cpu-features/deps/cpu_features/ndk_compat/README.md +4 -0
- package/node_modules/cpu-features/deps/cpu_features/ndk_compat/cpu-features.c +205 -0
- package/node_modules/cpu-features/deps/cpu_features/ndk_compat/cpu-features.h +320 -0
- package/node_modules/cpu-features/deps/cpu_features/ndk_compat/ndk-compat-test.c +12 -0
- package/node_modules/cpu-features/deps/cpu_features/patches/0001-Add-Apple-Silicon-Support.patch +456 -0
- package/node_modules/cpu-features/deps/cpu_features/scripts/generate_badges.d +165 -0
- package/node_modules/cpu-features/deps/cpu_features/scripts/make_release.sh +75 -0
- package/node_modules/cpu-features/deps/cpu_features/scripts/run_integration.sh +490 -0
- package/node_modules/cpu-features/deps/cpu_features/scripts/test_integration.sh +79 -0
- package/node_modules/cpu-features/deps/cpu_features/src/copy.inl +19 -0
- package/node_modules/cpu-features/deps/cpu_features/src/define_introspection.inl +86 -0
- package/node_modules/cpu-features/deps/cpu_features/src/define_introspection_and_hwcaps.inl +26 -0
- package/node_modules/cpu-features/deps/cpu_features/src/equals.inl +22 -0
- package/node_modules/cpu-features/deps/cpu_features/src/filesystem.c +62 -0
- package/node_modules/cpu-features/deps/cpu_features/src/hwcaps.c +169 -0
- package/node_modules/cpu-features/deps/cpu_features/src/impl_aarch64__base_implementation.inl +88 -0
- package/node_modules/cpu-features/deps/cpu_features/src/impl_aarch64_linux_or_android.c +79 -0
- package/node_modules/cpu-features/deps/cpu_features/src/impl_aarch64_macos_or_iphone.c +82 -0
- package/node_modules/cpu-features/deps/cpu_features/src/impl_aarch64_windows.c +138 -0
- package/node_modules/cpu-features/deps/cpu_features/src/impl_arm_linux_or_android.c +212 -0
- package/node_modules/cpu-features/deps/cpu_features/src/impl_mips_linux_or_android.c +93 -0
- package/node_modules/cpu-features/deps/cpu_features/src/impl_ppc_linux.c +163 -0
- package/node_modules/cpu-features/deps/cpu_features/src/impl_riscv_linux.c +111 -0
- package/node_modules/cpu-features/deps/cpu_features/src/impl_s390x_linux.c +120 -0
- package/node_modules/cpu-features/deps/cpu_features/src/impl_x86__base_implementation.inl +2055 -0
- package/node_modules/cpu-features/deps/cpu_features/src/impl_x86_freebsd.c +68 -0
- package/node_modules/cpu-features/deps/cpu_features/src/impl_x86_linux_or_android.c +58 -0
- package/node_modules/cpu-features/deps/cpu_features/src/impl_x86_macos.c +57 -0
- package/node_modules/cpu-features/deps/cpu_features/src/impl_x86_windows.c +58 -0
- package/node_modules/cpu-features/deps/cpu_features/src/stack_line_reader.c +132 -0
- package/node_modules/cpu-features/deps/cpu_features/src/string_view.c +192 -0
- package/node_modules/cpu-features/deps/cpu_features/src/utils/list_cpu_features.c +461 -0
- package/node_modules/cpu-features/deps/cpu_features/test/CMakeLists.txt +114 -0
- package/node_modules/cpu-features/deps/cpu_features/test/bit_utils_test.cc +53 -0
- package/node_modules/cpu-features/deps/cpu_features/test/cpuinfo_aarch64_test.cc +396 -0
- package/node_modules/cpu-features/deps/cpu_features/test/cpuinfo_arm_test.cc +384 -0
- package/node_modules/cpu-features/deps/cpu_features/test/cpuinfo_mips_test.cc +176 -0
- package/node_modules/cpu-features/deps/cpu_features/test/cpuinfo_ppc_test.cc +131 -0
- package/node_modules/cpu-features/deps/cpu_features/test/cpuinfo_riscv_test.cc +180 -0
- package/node_modules/cpu-features/deps/cpu_features/test/cpuinfo_s390x_test.cc +82 -0
- package/node_modules/cpu-features/deps/cpu_features/test/cpuinfo_x86_test.cc +1729 -0
- package/node_modules/cpu-features/deps/cpu_features/test/filesystem_for_testing.cc +103 -0
- package/node_modules/cpu-features/deps/cpu_features/test/filesystem_for_testing.h +61 -0
- package/node_modules/cpu-features/deps/cpu_features/test/hwcaps_for_testing.cc +52 -0
- package/node_modules/cpu-features/deps/cpu_features/test/hwcaps_for_testing.h +31 -0
- package/node_modules/cpu-features/deps/cpu_features/test/stack_line_reader_test.cc +132 -0
- package/node_modules/cpu-features/deps/cpu_features/test/string_view_test.cc +202 -0
- package/node_modules/cpu-features/lib/index.js +5 -0
- package/node_modules/cpu-features/package.json +40 -0
- package/node_modules/cpu-features/src/binding.cc +151 -0
- package/node_modules/cpu-features/test/test.js +12 -0
- package/node_modules/nan/.github/workflows/ci.yml +46 -0
- package/node_modules/nan/.pre-commit-config.yaml +8 -0
- package/node_modules/nan/CHANGELOG.md +605 -0
- package/node_modules/nan/CMakeLists.txt +138 -0
- package/node_modules/nan/LICENSE.md +9 -0
- package/node_modules/nan/README.md +457 -0
- package/node_modules/nan/doc/asyncworker.md +146 -0
- package/node_modules/nan/doc/buffers.md +54 -0
- package/node_modules/nan/doc/callback.md +76 -0
- package/node_modules/nan/doc/converters.md +41 -0
- package/node_modules/nan/doc/errors.md +226 -0
- package/node_modules/nan/doc/json.md +62 -0
- package/node_modules/nan/doc/maybe_types.md +583 -0
- package/node_modules/nan/doc/methods.md +689 -0
- package/node_modules/nan/doc/new.md +147 -0
- package/node_modules/nan/doc/node_misc.md +123 -0
- package/node_modules/nan/doc/object_wrappers.md +263 -0
- package/node_modules/nan/doc/persistent.md +296 -0
- package/node_modules/nan/doc/scopes.md +73 -0
- package/node_modules/nan/doc/script.md +58 -0
- package/node_modules/nan/doc/string_bytes.md +81 -0
- package/node_modules/nan/doc/v8_internals.md +211 -0
- package/node_modules/nan/doc/v8_misc.md +85 -0
- package/node_modules/nan/include_dirs.js +1 -0
- package/node_modules/nan/nan.h +3202 -0
- package/node_modules/nan/nan_callbacks.h +141 -0
- package/node_modules/nan/nan_callbacks_12_inl.h +730 -0
- package/node_modules/nan/nan_callbacks_pre_12_inl.h +528 -0
- package/node_modules/nan/nan_converters.h +72 -0
- package/node_modules/nan/nan_converters_43_inl.h +68 -0
- package/node_modules/nan/nan_converters_pre_43_inl.h +42 -0
- package/node_modules/nan/nan_define_own_property_helper.h +29 -0
- package/node_modules/nan/nan_implementation_12_inl.h +446 -0
- package/node_modules/nan/nan_implementation_pre_12_inl.h +263 -0
- package/node_modules/nan/nan_json.h +166 -0
- package/node_modules/nan/nan_maybe_43_inl.h +360 -0
- package/node_modules/nan/nan_maybe_pre_43_inl.h +268 -0
- package/node_modules/nan/nan_new.h +340 -0
- package/node_modules/nan/nan_object_wrap.h +156 -0
- package/node_modules/nan/nan_persistent_12_inl.h +132 -0
- package/node_modules/nan/nan_persistent_pre_12_inl.h +242 -0
- package/node_modules/nan/nan_private.h +73 -0
- package/node_modules/nan/nan_scriptorigin.h +97 -0
- package/node_modules/nan/nan_string_bytes.h +305 -0
- package/node_modules/nan/nan_typedarray_contents.h +96 -0
- package/node_modules/nan/nan_weak.h +453 -0
- package/node_modules/nan/package.json +38 -0
- package/node_modules/nan/tools/1to2.js +412 -0
- package/node_modules/nan/tools/README.md +14 -0
- package/node_modules/nan/tools/package.json +19 -0
- package/node_modules/safer-buffer/LICENSE +21 -0
- package/node_modules/safer-buffer/Porting-Buffer.md +268 -0
- package/node_modules/safer-buffer/Readme.md +156 -0
- package/node_modules/safer-buffer/dangerous.js +58 -0
- package/node_modules/safer-buffer/package.json +34 -0
- package/node_modules/safer-buffer/safer.js +77 -0
- package/node_modules/safer-buffer/tests.js +406 -0
- package/node_modules/ssh2/.eslintignore +4 -0
- package/node_modules/ssh2/.eslintrc.js +5 -0
- package/node_modules/ssh2/.github/workflows/ci.yml +110 -0
- package/node_modules/ssh2/.github/workflows/lint.yml +27 -0
- package/node_modules/ssh2/LICENSE +19 -0
- package/node_modules/ssh2/README.md +1529 -0
- package/node_modules/ssh2/SFTP.md +413 -0
- package/node_modules/ssh2/examples/server-chat.js +238 -0
- package/node_modules/ssh2/examples/sftp-server-download-only.js +134 -0
- package/node_modules/ssh2/install.js +27 -0
- package/node_modules/ssh2/lib/Channel.js +295 -0
- package/node_modules/ssh2/lib/agent.js +1123 -0
- package/node_modules/ssh2/lib/client.js +2176 -0
- package/node_modules/ssh2/lib/http-agents.js +84 -0
- package/node_modules/ssh2/lib/index.js +44 -0
- package/node_modules/ssh2/lib/keygen.js +582 -0
- package/node_modules/ssh2/lib/protocol/Protocol.js +2136 -0
- package/node_modules/ssh2/lib/protocol/SFTP.js +4052 -0
- package/node_modules/ssh2/lib/protocol/constants.js +356 -0
- package/node_modules/ssh2/lib/protocol/crypto/binding.gyp +23 -0
- package/node_modules/ssh2/lib/protocol/crypto/poly1305.js +43 -0
- package/node_modules/ssh2/lib/protocol/crypto/src/binding.cc +2300 -0
- package/node_modules/ssh2/lib/protocol/crypto.js +1602 -0
- package/node_modules/ssh2/lib/protocol/handlers.js +16 -0
- package/node_modules/ssh2/lib/protocol/handlers.misc.js +1285 -0
- package/node_modules/ssh2/lib/protocol/kex.js +1908 -0
- package/node_modules/ssh2/lib/protocol/keyParser.js +1484 -0
- package/node_modules/ssh2/lib/protocol/node-fs-compat.js +115 -0
- package/node_modules/ssh2/lib/protocol/utils.js +356 -0
- package/node_modules/ssh2/lib/protocol/zlib.js +255 -0
- package/node_modules/ssh2/lib/server.js +1380 -0
- package/node_modules/ssh2/lib/utils.js +336 -0
- package/node_modules/ssh2/package.json +49 -0
- package/node_modules/ssh2/test/common.js +316 -0
- package/node_modules/ssh2/test/fixtures/bad_rsa_private_key +26 -0
- package/node_modules/ssh2/test/fixtures/https_cert.pem +33 -0
- package/node_modules/ssh2/test/fixtures/https_key.pem +52 -0
- package/node_modules/ssh2/test/fixtures/id_dsa +12 -0
- package/node_modules/ssh2/test/fixtures/id_ecdsa +5 -0
- package/node_modules/ssh2/test/fixtures/id_rsa +15 -0
- package/node_modules/ssh2/test/fixtures/id_rsa.ppk +26 -0
- package/node_modules/ssh2/test/fixtures/id_rsa_enc +30 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_dsa +21 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_dsa.pub +1 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_dsa.pub.result +7 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_dsa.result +7 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_dsa_enc +22 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_dsa_enc.pub +1 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_dsa_enc.pub.result +7 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_dsa_enc.result +7 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_dsa_enc_gcm +23 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_dsa_enc_gcm.pub +1 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_dsa_enc_gcm.pub.result +7 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_dsa_enc_gcm.result +7 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_ecdsa +9 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_ecdsa.pub +1 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_ecdsa.pub.result +7 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_ecdsa.result +7 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_ecdsa_enc +10 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_ecdsa_enc.pub +1 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_ecdsa_enc.pub.result +8 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_ecdsa_enc.result +7 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_ecdsa_enc_gcm +10 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_ecdsa_enc_gcm.pub +1 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_ecdsa_enc_gcm.pub.result +8 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_ecdsa_enc_gcm.result +7 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_ed25519 +7 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_ed25519.pub +1 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_ed25519.pub.result +7 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_ed25519.result +7 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_rsa +27 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_rsa.pub +1 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_rsa.pub.result +7 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_rsa.result +7 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_rsa_enc +28 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_rsa_enc.pub +1 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_rsa_enc.pub.result +8 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_rsa_enc.result +7 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_rsa_enc_gcm +29 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_rsa_enc_gcm.pub +1 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_rsa_enc_gcm.pub.result +8 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_new_rsa_enc_gcm.result +7 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_old_dsa +12 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_old_dsa.pub +1 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_old_dsa.pub.result +7 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_old_dsa.result +7 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_old_dsa_enc +15 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_old_dsa_enc.pub +1 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_old_dsa_enc.pub.result +8 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_old_dsa_enc.result +7 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_old_ecdsa +5 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_old_ecdsa.pub +1 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_old_ecdsa.pub.result +7 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_old_ecdsa.result +7 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_old_ecdsa_enc +8 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_old_ecdsa_enc.pub +1 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_old_ecdsa_enc.pub.result +8 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_old_ecdsa_enc.result +7 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_old_rsa +27 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_old_rsa.pub +1 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_old_rsa.pub.result +7 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_old_rsa.result +7 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_old_rsa_enc +30 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_old_rsa_enc.pub +1 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_old_rsa_enc.pub.result +8 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_old_rsa_enc.result +7 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_old_rsa_enc_aes256 +54 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_old_rsa_enc_aes256.pub +1 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_old_rsa_enc_aes256.pub.result +6 -0
- package/node_modules/ssh2/test/fixtures/keyParser/openssh_old_rsa_enc_aes256.result +6 -0
- package/node_modules/ssh2/test/fixtures/keyParser/ppk_dsa_enc +17 -0
- package/node_modules/ssh2/test/fixtures/keyParser/ppk_dsa_enc.result +7 -0
- package/node_modules/ssh2/test/fixtures/keyParser/ppk_rsa +26 -0
- package/node_modules/ssh2/test/fixtures/keyParser/ppk_rsa.result +7 -0
- package/node_modules/ssh2/test/fixtures/keyParser/ppk_rsa_enc +18 -0
- package/node_modules/ssh2/test/fixtures/keyParser/ppk_rsa_enc.result +7 -0
- package/node_modules/ssh2/test/fixtures/keyParser/rfc4716_rsa.pub +9 -0
- package/node_modules/ssh2/test/fixtures/keyParser/rfc4716_rsa.pub.result +7 -0
- package/node_modules/ssh2/test/fixtures/keyParser/rfc4716_rsa2.pub +10 -0
- package/node_modules/ssh2/test/fixtures/keyParser/rfc4716_rsa2.pub.result +7 -0
- package/node_modules/ssh2/test/fixtures/keyParser/rfc4716_rsa3.pub +11 -0
- package/node_modules/ssh2/test/fixtures/keyParser/rfc4716_rsa3.pub.result +7 -0
- package/node_modules/ssh2/test/fixtures/keyParser/rfc4716_rsa4.pub +11 -0
- package/node_modules/ssh2/test/fixtures/keyParser/rfc4716_rsa4.pub.result +7 -0
- package/node_modules/ssh2/test/fixtures/keyParser/rfc4716_rsa5.pub +8 -0
- package/node_modules/ssh2/test/fixtures/keyParser/rfc4716_rsa5.pub.result +7 -0
- package/node_modules/ssh2/test/fixtures/keyParser/rfc4716_rsa6.pub +13 -0
- package/node_modules/ssh2/test/fixtures/keyParser/rfc4716_rsa6.pub.result +7 -0
- package/node_modules/ssh2/test/fixtures/openssh_new_rsa +27 -0
- package/node_modules/ssh2/test/fixtures/ssh_host_dsa_key +12 -0
- package/node_modules/ssh2/test/fixtures/ssh_host_ecdsa_key +5 -0
- package/node_modules/ssh2/test/fixtures/ssh_host_rsa_key +15 -0
- package/node_modules/ssh2/test/test-exec.js +578 -0
- package/node_modules/ssh2/test/test-integration-openssh.js +486 -0
- package/node_modules/ssh2/test/test-keygen.js +60 -0
- package/node_modules/ssh2/test/test-misc-client-server.js +1460 -0
- package/node_modules/ssh2/test/test-openssh.js +261 -0
- package/node_modules/ssh2/test/test-protocol-crypto.js +631 -0
- package/node_modules/ssh2/test/test-protocol-keyparser.js +177 -0
- package/node_modules/ssh2/test/test-server-hostkeys.js +138 -0
- package/node_modules/ssh2/test/test-sftp.js +842 -0
- package/node_modules/ssh2/test/test-shell.js +109 -0
- package/node_modules/ssh2/test/test-userauth-agent-openssh.js +110 -0
- package/node_modules/ssh2/test/test-userauth-agent.js +171 -0
- package/node_modules/ssh2/test/test-userauth.js +611 -0
- package/node_modules/ssh2/test/test-worker-imports.js +25 -0
- package/node_modules/ssh2/test/test.js +20 -0
- package/node_modules/ssh2/util/build_pagent.bat +2 -0
- package/node_modules/ssh2/util/pagent.c +88 -0
- package/node_modules/ssh2/util/pagent.exe +0 -0
- package/node_modules/tweetnacl/.npmignore +4 -0
- package/node_modules/tweetnacl/AUTHORS.md +28 -0
- package/node_modules/tweetnacl/CHANGELOG.md +221 -0
- package/node_modules/tweetnacl/LICENSE +24 -0
- package/node_modules/tweetnacl/PULL_REQUEST_TEMPLATE.md +20 -0
- package/node_modules/tweetnacl/README.md +459 -0
- package/node_modules/tweetnacl/nacl-fast.js +2388 -0
- package/node_modules/tweetnacl/nacl-fast.min.js +2 -0
- package/node_modules/tweetnacl/nacl.d.ts +98 -0
- package/node_modules/tweetnacl/nacl.js +1175 -0
- package/node_modules/tweetnacl/nacl.min.js +1 -0
- package/node_modules/tweetnacl/package.json +58 -0
- package/package.json +96 -89
package/lib/update.js
CHANGED
|
@@ -1,472 +1,472 @@
|
|
|
1
|
-
// dsh-remote — self-update support.
|
|
2
|
-
//
|
|
3
|
-
// dsh-remote is a zero-build plugin: the host half (lib/index.js) is loaded
|
|
4
|
-
// straight from the installed package, so "updating" = replacing the package
|
|
5
|
-
// files with the newer npm tarball. This module owns the version check and the
|
|
6
|
-
// tarball apply:
|
|
7
|
-
//
|
|
8
|
-
// • checkLatestVersion() — query the npm registry for the `latest` dist-tag;
|
|
9
|
-
// • applyUpdate() — download the tarball, extract package/{lib,package.json,
|
|
10
|
-
// cordis.patch.yml} with a minimal tar reader, verify the version, then
|
|
11
|
-
// replace the installed files atomically (temp file + rename, never a
|
|
12
|
-
// half-written lib/*.js). A `.dsh-remote-updated` marker records the version
|
|
13
|
-
// that landed; the host half removes it once that version is the code it is
|
|
14
|
-
// actually running (see LOADED_VERSION), so the marker means "restart still
|
|
15
|
-
// required" rather than "an update once happened".
|
|
16
|
-
// • reloadSelf() — swap the RUNNING host half to the code now on disk: drop
|
|
17
|
-
// Node's module caches for this package's own files, then dispose + re-init
|
|
18
|
-
// the loader entry so apply() runs again from the fresh module. Without it
|
|
19
|
-
// an update only takes effect on the next process start, which is what left
|
|
20
|
-
// the served client half newer than the loaded host half.
|
|
21
|
-
//
|
|
22
|
-
// Every failure aborts without touching the installed files (a bad download or
|
|
23
|
-
// a version mismatch must never break the running plugin).
|
|
24
|
-
|
|
25
|
-
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
|
26
|
-
import { createRequire } from 'node:module'
|
|
27
|
-
import path from 'node:path'
|
|
28
|
-
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
29
|
-
|
|
30
|
-
const NPM_PACKAGE = 'dsh-remote'
|
|
31
|
-
const REGISTRY_URL = 'https://registry.npmjs.org/' + NPM_PACKAGE + '/latest'
|
|
32
|
-
|
|
33
|
-
/** Absolute directory holding this plugin's installed files (lib/ + package.json). */
|
|
34
|
-
export function selfDir() {
|
|
35
|
-
try {
|
|
36
|
-
return path.dirname(path.dirname(fileURLToPath(import.meta.url)))
|
|
37
|
-
} catch {
|
|
38
|
-
return path.dirname(path.dirname(process.argv[1] || ''))
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* 本插件是否装在 `node_modules` 之下(即真正的"安装副本")。
|
|
44
|
-
*
|
|
45
|
-
* ★ 为什么必须有这道判断(实测依据,不是防御性猜测):
|
|
46
|
-
* `selfDir()` 基于 `import.meta.url`,而 **Node 的 ESM 解析会对符号链接做
|
|
47
|
-
* realpath**(已实测:经由 symlink 导入时 `import.meta.url` 报告的是
|
|
48
|
-
* `/real/mod.mjs` 而不是 `/link/mod.mjs`)。于是当插件以 `link:` / 开发模式
|
|
49
|
-
* 安装时,`selfDir()` 指向的是**用户的源码仓库**,而不是安装副本。
|
|
50
|
-
* 一旦 `auto` 更新落地,`applyUpdate` 就会拿 npm 包覆盖那份源码
|
|
51
|
-
* (丢改动、脏工作树)。默认改成 `auto` 之后这个风险从"手点才触发"
|
|
52
|
-
* 变成"自动发生",所以在此拦下:不在 node_modules 下就不自更新。
|
|
53
|
-
*
|
|
54
|
-
* 代价:少数把插件放在非标准路径的部署不会被自动更新——他们仍可手动更新。
|
|
55
|
-
* 这个方向的取舍是正确的:宁可少更新,也不能悄悄毁掉源码。
|
|
56
|
-
* @param dir - 要判定的目录(默认本插件的 selfDir())。
|
|
57
|
-
*/
|
|
58
|
-
export function isInstalledCopy(dir = selfDir()) {
|
|
59
|
-
// 同时认 `/` 与 `\`:本函数要对两种平台的路径都给出正确答案,而调用方
|
|
60
|
-
// (尤其测试)会传另一种分隔符的路径。先前只按 path.sep 切分,导致
|
|
61
|
-
// 在 Linux 上判断 Windows 风格路径时误判为"非安装副本"(CI 直接红了)。
|
|
62
|
-
const normalized = String(dir).replace(/\\/g, '/').toLowerCase()
|
|
63
|
-
return normalized.includes('/node_modules/')
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** Read the installed package.json version; "0.0.0" when unreadable. */
|
|
67
|
-
export function readVersion() {
|
|
68
|
-
try {
|
|
69
|
-
const pkg = JSON.parse(readFileSync(path.join(selfDir(), 'package.json'), 'utf8'))
|
|
70
|
-
return String(pkg.version || '0.0.0')
|
|
71
|
-
} catch {
|
|
72
|
-
return '0.0.0'
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/** Version of the code this process actually loaded (snapshot at import time).
|
|
77
|
-
* `readVersion()` reads package.json from disk, so after an update lands it
|
|
78
|
-
* reports the NEW version while the running module is still the old one —
|
|
79
|
-
* comparing the two is the only way to tell "applied" from "active". */
|
|
80
|
-
export const LOADED_VERSION = readVersion()
|
|
81
|
-
|
|
82
|
-
/** Package version currently on disk (may be newer than {@link LOADED_VERSION}). */
|
|
83
|
-
export function diskVersion() {
|
|
84
|
-
return readVersion()
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/** Compare dotted versions; returns true when a > b. */
|
|
88
|
-
export function gtVersion(a, b) {
|
|
89
|
-
const pa = String(a || '').split('.').map((n) => parseInt(n, 10) || 0)
|
|
90
|
-
const pb = String(b || '').split('.').map((n) => parseInt(n, 10) || 0)
|
|
91
|
-
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
92
|
-
const da = pa[i] || 0
|
|
93
|
-
const db = pb[i] || 0
|
|
94
|
-
if (da !== db) return da > db
|
|
95
|
-
}
|
|
96
|
-
return false
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* 查询 npm registry 的最新版本。
|
|
101
|
-
*
|
|
102
|
-
* 返回 `{ version }` 或 `{ error }`(**不再把一切失败压成 null**)。
|
|
103
|
-
*
|
|
104
|
-
* 为什么要区分失败原因:原先任何失败都返回 `null`,调用方只能笼统报
|
|
105
|
-
* "无法连接 npm registry"。实测踩过:本机被 npm 限流(`/latest` 返回 **429**,
|
|
106
|
-
* 而同一时刻 registry 根路径是 200、CI 从另一个出口 IP 也能正常取到),
|
|
107
|
-
* 却被显示成"连不上"——把"等一下就好"误导成"网络坏了"。429/超时/离线
|
|
108
|
-
* 三者的处置完全不同,必须让用户看到真实原因。
|
|
109
|
-
* @param timeoutMs - 超时(看门狗只负责中止,不影响进程存活)。
|
|
110
|
-
*/
|
|
111
|
-
export async function fetchLatestVersion(timeoutMs = 8000) {
|
|
112
|
-
try {
|
|
113
|
-
const controller = new AbortController()
|
|
114
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs)
|
|
115
|
-
// 纯看门狗:调用方并不 await 它,所以必须 unref,否则这一段超时窗口
|
|
116
|
-
// 会拖住事件循环、让本该退出的短命进程(脚本、测试)多挂数秒。
|
|
117
|
-
if (typeof timer.unref === 'function') timer.unref()
|
|
118
|
-
let res
|
|
119
|
-
try {
|
|
120
|
-
res = await fetch(REGISTRY_URL, { signal: controller.signal, headers: { accept: 'application/json' } })
|
|
121
|
-
} finally {
|
|
122
|
-
clearTimeout(timer)
|
|
123
|
-
}
|
|
124
|
-
if (res.status === 429) {
|
|
125
|
-
return { error: 'npm registry 限流(HTTP 429)——请求过于频繁,稍后会自动恢复', reason: 'rate-limited', status: 429 }
|
|
126
|
-
}
|
|
127
|
-
if (!res.ok) {
|
|
128
|
-
return { error: `npm registry 返回 HTTP ${res.status}`, reason: 'http', status: res.status }
|
|
129
|
-
}
|
|
130
|
-
const data = await res.json().catch(() => null)
|
|
131
|
-
const version = typeof data?.version === 'string' && data.version ? data.version : null
|
|
132
|
-
if (!version) return { error: 'npm registry 响应里没有可用的版本号', reason: 'malformed' }
|
|
133
|
-
return { version }
|
|
134
|
-
} catch (err) {
|
|
135
|
-
const aborted = err?.name === 'AbortError'
|
|
136
|
-
return {
|
|
137
|
-
error: aborted ? `连接 npm registry 超时(${timeoutMs}ms)` : '无法连接 npm registry',
|
|
138
|
-
reason: aborted ? 'timeout' : 'network',
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/** Minimal POSIX ustar reader (regular files only) — npm tarballs are gzipped tar. */
|
|
144
|
-
function parseTar(buf) {
|
|
145
|
-
const files = []
|
|
146
|
-
let off = 0
|
|
147
|
-
while (off + 512 <= buf.length) {
|
|
148
|
-
const header = buf.subarray(off, off + 512)
|
|
149
|
-
const name = header.subarray(0, 100).toString('utf8').replace(/\0.*$/, '')
|
|
150
|
-
if (!name) break
|
|
151
|
-
const sizeStr = header.subarray(124, 136).toString('utf8').replace(/\0.*$/, '').trim()
|
|
152
|
-
const size = parseInt(sizeStr, 8) || 0
|
|
153
|
-
const type = String.fromCharCode(header[156] || 48)
|
|
154
|
-
off += 512
|
|
155
|
-
const data = type === '48' || type === '0' ? Buffer.from(buf.subarray(off, off + size)) : null
|
|
156
|
-
if (data) files.push({ name, data })
|
|
157
|
-
off += Math.ceil(size / 512) * 512
|
|
158
|
-
}
|
|
159
|
-
return files
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Download the given version's tarball and atomically replace the installed
|
|
164
|
-
* package files (lib/*.js, cordis.patch.yml, package.json). On success writes
|
|
165
|
-
* `.dsh-remote-updated` in the install dir; throws on any failure (leaves the
|
|
166
|
-
* installed files untouched).
|
|
167
|
-
* @param targetVersion - exact npm version to install (e.g. "0.8.0").
|
|
168
|
-
*/
|
|
169
|
-
export async function applyUpdate(targetVersion, options = {}) {
|
|
170
|
-
// 拒绝对非安装副本自更新:`auto` 默认开启后,一次自动更新就会覆盖
|
|
171
|
-
// `selfDir()` 指向的目录,而以 link:/开发模式安装时那正是用户的源码仓库
|
|
172
|
-
// (见 isInstalledCopy 的实测说明)。测试用 options.dir 指向临时目录,
|
|
173
|
-
// 因此不受此限(options.dir 存在即视为显式指定,跳过检查)。
|
|
174
|
-
if (!options.dir && !isInstalledCopy()) {
|
|
175
|
-
throw new Error('refusing to self-update: this copy is not under node_modules (link/dev install) — update it from its source repo instead')
|
|
176
|
-
}
|
|
177
|
-
const tarballUrl = `https://registry.npmjs.org/${NPM_PACKAGE}/-/${NPM_PACKAGE}-${targetVersion}.tgz`
|
|
178
|
-
const dir = options.dir || selfDir()
|
|
179
|
-
const doFetch = options.fetchImpl || fetch
|
|
180
|
-
const tmpRoot = path.join(dir, `.dsh-remote-update-${Date.now()}`)
|
|
181
|
-
const tmpPkg = path.join(tmpRoot, 'package')
|
|
182
|
-
try {
|
|
183
|
-
const controller = new AbortController()
|
|
184
|
-
const timer = setTimeout(() => controller.abort(), 60000)
|
|
185
|
-
let res
|
|
186
|
-
try {
|
|
187
|
-
res = await doFetch(tarballUrl, { signal: controller.signal })
|
|
188
|
-
} finally {
|
|
189
|
-
clearTimeout(timer)
|
|
190
|
-
}
|
|
191
|
-
if (!res.ok) throw new Error(`download failed (HTTP ${res.status})`)
|
|
192
|
-
const buf = Buffer.from(await res.arrayBuffer())
|
|
193
|
-
// npm tarballs are gzipped tar; extract the package/ directory.
|
|
194
|
-
const { gunzipSync } = await import('node:zlib')
|
|
195
|
-
const tar = gunzipSync(buf)
|
|
196
|
-
const files = parseTar(tar)
|
|
197
|
-
mkdirSync(tmpPkg, { recursive: true })
|
|
198
|
-
let extracted = 0
|
|
199
|
-
for (const f of files) {
|
|
200
|
-
const rel = f.name.replace(/^package\//, '')
|
|
201
|
-
if (!rel || rel === 'package/' || rel.endsWith('/')) continue
|
|
202
|
-
if (rel !== 'package.json' && rel !== 'cordis.patch.yml' && !rel.startsWith('lib/')) continue
|
|
203
|
-
const dest = path.join(tmpPkg, rel)
|
|
204
|
-
mkdirSync(path.dirname(dest), { recursive: true })
|
|
205
|
-
writeFileSync(dest, f.data)
|
|
206
|
-
extracted++
|
|
207
|
-
}
|
|
208
|
-
if (extracted === 0) throw new Error('tarball contained no package files')
|
|
209
|
-
// Verify the downloaded package.json matches the requested version.
|
|
210
|
-
const newPkg = JSON.parse(readFileSync(path.join(tmpPkg, 'package.json'), 'utf8'))
|
|
211
|
-
if (newPkg.version !== targetVersion) throw new Error(`tarball version mismatch: ${newPkg.version}`)
|
|
212
|
-
// Sanity: the new host half must at least parse (a corrupt index.js would
|
|
213
|
-
// break the next boot — check the file size and that it is not empty).
|
|
214
|
-
const newIndex = path.join(tmpPkg, 'lib', 'index.js')
|
|
215
|
-
if (!existsSync(newIndex) || statSize(newIndex) < 100) throw new Error('tarball missing lib/index.js')
|
|
216
|
-
// Swap the installed files one at a time: atomic, and idempotent.
|
|
217
|
-
//
|
|
218
|
-
// Atomicity (temp file + rename) is not cosmetic here — since the browser
|
|
219
|
-
// half is hot-swapped by dsh-client-hmr, which stat-polls lib/client.js
|
|
220
|
-
// every 500 ms, a torn write would be re-hashed and served mid-copy.
|
|
221
|
-
// Skipping byte-identical files keeps a re-apply of the same version from
|
|
222
|
-
// churning mtimes (and the host half from reloading for nothing).
|
|
223
|
-
const installLib = path.join(dir, 'lib')
|
|
224
|
-
const tmpLib = path.join(tmpPkg, 'lib')
|
|
225
|
-
const changed = []
|
|
226
|
-
const install = (src, dest) => {
|
|
227
|
-
const bytes = readFileSync(src)
|
|
228
|
-
if (sameFileContent(dest, bytes)) return
|
|
229
|
-
try { mkdirSync(path.dirname(dest), { recursive: true }) } catch {}
|
|
230
|
-
writeFileAtomic(dest, bytes)
|
|
231
|
-
changed.push(path.relative(dir, dest).split(path.sep).join('/'))
|
|
232
|
-
}
|
|
233
|
-
for (const name of readdirSafe(tmpLib)) {
|
|
234
|
-
if (!name.endsWith('.js')) continue
|
|
235
|
-
const src = path.join(tmpLib, name)
|
|
236
|
-
if (existsSync(src)) install(src, path.join(installLib, name))
|
|
237
|
-
}
|
|
238
|
-
for (const rel of ['package.json', 'cordis.patch.yml']) {
|
|
239
|
-
const src = path.join(tmpPkg, rel)
|
|
240
|
-
if (existsSync(src)) install(src, path.join(dir, rel))
|
|
241
|
-
}
|
|
242
|
-
writeFileSync(path.join(dir, '.dsh-remote-updated'), String(targetVersion))
|
|
243
|
-
return { ok: true, to: targetVersion, changed }
|
|
244
|
-
} catch (err) {
|
|
245
|
-
throw new Error('update failed: ' + ((err && err.message) || err))
|
|
246
|
-
} finally {
|
|
247
|
-
try { rmSync(tmpRoot, { recursive: true, force: true }) } catch {}
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/** File size helper (missing/unreadable → 0). */
|
|
252
|
-
function statSize(p) {
|
|
253
|
-
try {
|
|
254
|
-
return statSync(p).size
|
|
255
|
-
} catch {
|
|
256
|
-
return 0
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
/** readdir helper that returns [] on failure. */
|
|
261
|
-
function readdirSafe(dir) {
|
|
262
|
-
try {
|
|
263
|
-
return readdirSync(dir)
|
|
264
|
-
} catch {
|
|
265
|
-
return []
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/**
|
|
270
|
-
* Write `data` to `dest` atomically: the bytes land in a sibling temp file and
|
|
271
|
-
* are then renamed over the target, so a concurrent reader (the client-half HMR
|
|
272
|
-
* poll, or the next boot) never observes a truncated file. Falls back to an
|
|
273
|
-
* in-place copy if the platform refuses the replace, and never leaves the temp
|
|
274
|
-
* file behind.
|
|
275
|
-
* @param dest - absolute destination path.
|
|
276
|
-
* @param data - bytes to write.
|
|
277
|
-
*/
|
|
278
|
-
export function writeFileAtomic(dest, data) {
|
|
279
|
-
const tmp = `${dest}.dsh-tmp-${process.pid}-${Date.now().toString(36)}`
|
|
280
|
-
writeFileSync(tmp, data)
|
|
281
|
-
try {
|
|
282
|
-
renameSync(tmp, dest)
|
|
283
|
-
return
|
|
284
|
-
} catch {
|
|
285
|
-
try {
|
|
286
|
-
copyFileSync(tmp, dest)
|
|
287
|
-
} finally {
|
|
288
|
-
try { rmSync(tmp, { force: true }) } catch {}
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
/** Whether the file at `p` already holds exactly `bytes` (missing/short → false). */
|
|
294
|
-
function sameFileContent(p, bytes) {
|
|
295
|
-
try {
|
|
296
|
-
return readFileSync(p).equals(bytes)
|
|
297
|
-
} catch {
|
|
298
|
-
return false
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// ── hot swap of the running host half ─────────────────────────────────────
|
|
303
|
-
|
|
304
|
-
/** Loader entry ids this package can own. */
|
|
305
|
-
const SELF_ENTRY_NAMES = ['dsh-remote']
|
|
306
|
-
|
|
307
|
-
/**
|
|
308
|
-
* Find our own loader entry (by id, then by module specifier). Returns undefined
|
|
309
|
-
* when the tree does not contain us (e.g. a test loader, or a renamed row).
|
|
310
|
-
* @param loader - the cordis Loader service (`ctx.loader`).
|
|
311
|
-
*/
|
|
312
|
-
function findSelfEntry(loader) {
|
|
313
|
-
let entries = []
|
|
314
|
-
try {
|
|
315
|
-
entries = [...(loader?.entries?.() ?? [])]
|
|
316
|
-
} catch {
|
|
317
|
-
return undefined
|
|
318
|
-
}
|
|
319
|
-
for (const want of SELF_ENTRY_NAMES) {
|
|
320
|
-
const byId = entries.find((entry) => entry?.options?.id === want)
|
|
321
|
-
if (byId) return byId
|
|
322
|
-
}
|
|
323
|
-
for (const want of SELF_ENTRY_NAMES) {
|
|
324
|
-
const byName = entries.find((entry) => entry?.options?.name === want)
|
|
325
|
-
if (byName) return byName
|
|
326
|
-
}
|
|
327
|
-
return undefined
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
/**
|
|
331
|
-
* Drop Node's module caches for this package's own files, so the next import of
|
|
332
|
-
* the plugin actually re-reads disk instead of handing back the cached module.
|
|
333
|
-
* Both caches matter: the ESM `loadCache` (a plain Map on Node 22/23, a
|
|
334
|
-
* `LoadCache extends Map` on Node 24 — hence the explicit `Map.prototype` calls)
|
|
335
|
-
* and, for CJS modules pulled in through `import()`, `require.cache`.
|
|
336
|
-
* @param loader - the cordis Loader service.
|
|
337
|
-
* @param dir - package directory whose `lib/` to evict (defaults to this
|
|
338
|
-
* package's own install dir; overridable so the swap can be exercised against
|
|
339
|
-
* a fixture install).
|
|
340
|
-
* @returns how many ESM entries were dropped.
|
|
341
|
-
*/
|
|
342
|
-
export function clearSelfModuleCache(loader, dir = selfDir()) {
|
|
343
|
-
const libDir = path.join(dir, 'lib')
|
|
344
|
-
const loadCache = loader?.internal?.loadCache
|
|
345
|
-
const req = (() => {
|
|
346
|
-
try { return createRequire(import.meta.url) } catch { return undefined }
|
|
347
|
-
})()
|
|
348
|
-
let cleared = 0
|
|
349
|
-
for (const name of readdirSafe(libDir)) {
|
|
350
|
-
if (!name.endsWith('.js')) continue
|
|
351
|
-
const url = pathToFileURL(path.join(libDir, name)).href
|
|
352
|
-
try {
|
|
353
|
-
if (loadCache && Map.prototype.has.call(loadCache, url)) {
|
|
354
|
-
Map.prototype.delete.call(loadCache, url)
|
|
355
|
-
cleared += 1
|
|
356
|
-
}
|
|
357
|
-
} catch {}
|
|
358
|
-
try {
|
|
359
|
-
const file = fileURLToPath(url)
|
|
360
|
-
if (req?.cache && req.cache[file]) delete req.cache[file]
|
|
361
|
-
} catch {}
|
|
362
|
-
}
|
|
363
|
-
return cleared
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
/**
|
|
367
|
-
* Swap the RUNNING host half to the code now on disk: clear the module caches
|
|
368
|
-
* for our own files, dispose our loader entry (tools, JSON routes, SSH pools and
|
|
369
|
-
* every `ctx.effect` disposer go with the fiber), then re-init it so `apply()`
|
|
370
|
-
* runs again from the freshly imported module.
|
|
371
|
-
*
|
|
372
|
-
* Callers must not depend on this plugin after a successful reload — the fiber
|
|
373
|
-
* that served the current request is gone. Schedule it (see
|
|
374
|
-
* {@link scheduleSelfReload}) rather than awaiting it from a request handler.
|
|
375
|
-
* @param loader - the cordis Loader service (`ctx.loader`).
|
|
376
|
-
* @returns a small result object; never throws.
|
|
377
|
-
*/
|
|
378
|
-
export async function reloadSelf(loader) {
|
|
379
|
-
const entry = findSelfEntry(loader)
|
|
380
|
-
if (!entry) return { ok: false, reason: 'own loader entry not found' }
|
|
381
|
-
try {
|
|
382
|
-
const cleared = clearSelfModuleCache(loader)
|
|
383
|
-
// ★ 卸载必须走 entry.fiber.dispose()。
|
|
384
|
-
//
|
|
385
|
-
// 原实现找 `entry._dispose` / `entry.dispose`,但 cordis 的 Entry **两个都没有**
|
|
386
|
-
// (0.2.0 实测方法集只有 constructor/disabledOf/evaluate/_patchContext/refresh/
|
|
387
|
-
// update/_commitVolatile/init/_init),所以这里每次都直接返回
|
|
388
|
-
// "entry exposes no dispose hook" —— 热切换从未真正执行过,而
|
|
389
|
-
// scheduleSelfReload 里的 `.catch(() => {})` 把它彻底吞掉,
|
|
390
|
-
// UI 于是永远停在"已落盘但未切换"(用户报的 0.8.35 -> 0.8.36)。
|
|
391
|
-
//
|
|
392
|
-
// 也不能用 entry.refresh():它是 `if (this.fiber) return; await this.init()`,
|
|
393
|
-
// fiber 还活着时直接返回,等于没重启。真正卸载 fiber 的地方在 Entry.update()
|
|
394
|
-
// 里(`this.fiber?.dispose()`),这里照它做。
|
|
395
|
-
//
|
|
396
|
-
// 保留对 _dispose/dispose 的探测:那是宿主可能提供的扩展钩子,有则优先用,
|
|
397
|
-
// 没有就用 fiber.dispose()——两条路都必须能真正卸载,否则应如实报错。
|
|
398
|
-
// DSH 0.2 的 loader 在 fiber 被卸载时把 entry.options.disabled 写成 true
|
|
399
|
-
// 并写回配置树。客户端扫描会跳过禁用行,于是热切换之后宿主能起来、
|
|
400
|
-
// 设置页的 client 不再下发。这里记下卸载前的状态:只有 loader 在
|
|
401
|
-
// dispose 期间新打上的禁用才清掉。用户本来就禁用的行不要重新打开。
|
|
402
|
-
const disabledBefore = entry.options?.disabled === true
|
|
403
|
-
if (typeof entry._dispose === 'function') await entry._dispose()
|
|
404
|
-
else if (typeof entry.dispose === 'function') await entry.dispose()
|
|
405
|
-
else if (entry.fiber && typeof entry.fiber.dispose === 'function') await entry.fiber.dispose()
|
|
406
|
-
else return { ok: false, reason: 'entry exposes no dispose hook' }
|
|
407
|
-
if (!disabledBefore && entry.options?.disabled === true) {
|
|
408
|
-
delete entry.options.disabled
|
|
409
|
-
entry.parent?.tree?.write?.()
|
|
410
|
-
}
|
|
411
|
-
await entry.init()
|
|
412
|
-
return { ok: true, cleared }
|
|
413
|
-
} catch (err) {
|
|
414
|
-
return { ok: false, reason: String((err && err.message) || err) }
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
/**
|
|
419
|
-
* Fire-and-forget {@link reloadSelf} after a short delay, so the caller can
|
|
420
|
-
* finish answering an HTTP request (or an update check) before its own module is
|
|
421
|
-
* torn down. A failed swap leaves the running code in place, which is the
|
|
422
|
-
* pre-hot-update behaviour, not a crash — so we do not throw.
|
|
423
|
-
*
|
|
424
|
-
* We DO record the outcome, though. Silently discarding it is precisely how the
|
|
425
|
-
* "entry exposes no dispose hook" defect stayed invisible: the panel reported a
|
|
426
|
-
* scheduled swap, the swap never ran, and nothing anywhere said so.
|
|
427
|
-
* {@link lastSelfReload} exposes the result for diagnostics (the settings panel
|
|
428
|
-
* and `/dsh-remote/update-check` surface it), and a non-ok result is logged once.
|
|
429
|
-
*
|
|
430
|
-
* @param loader - the cordis Loader service.
|
|
431
|
-
* @param delayMs - grace period before the swap (default 300 ms).
|
|
432
|
-
* @returns true when the swap was scheduled.
|
|
433
|
-
*/
|
|
434
|
-
export function scheduleSelfReload(loader, delayMs = 300) {
|
|
435
|
-
const timer = setTimeout(() => {
|
|
436
|
-
void reloadSelf(loader).then(
|
|
437
|
-
(res) => { lastReloadResult = res; if (res && res.ok === false) console.warn('[dsh-remote] self-update swap failed:', res.reason) },
|
|
438
|
-
(err) => { lastReloadResult = { ok: false, reason: String((err && err.message) || err) }; console.warn('[dsh-remote] self-update swap threw:', lastReloadResult.reason) },
|
|
439
|
-
)
|
|
440
|
-
}, Math.max(0, delayMs))
|
|
441
|
-
if (typeof timer.unref === 'function') timer.unref()
|
|
442
|
-
return true
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
/** Result of the most recent {@link scheduleSelfReload} attempt, for diagnostics. */
|
|
446
|
-
let lastReloadResult = null
|
|
447
|
-
|
|
448
|
-
/** @returns the last self-reload outcome (`{ok,reason?}`, or null if never attempted). */
|
|
449
|
-
export function lastSelfReload() {
|
|
450
|
-
return lastReloadResult
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
/** Write the persisted update-mode override (settings UI). */
|
|
454
|
-
export function persistUpdateMode(mode) {
|
|
455
|
-
if (!['manual', 'auto', 'off'].includes(mode)) return false
|
|
456
|
-
try {
|
|
457
|
-
writeFileSync(path.join(selfDir(), 'update-mode'), mode)
|
|
458
|
-
return true
|
|
459
|
-
} catch {
|
|
460
|
-
return false
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
/** Read a persisted update-mode override, or null when absent/invalid. */
|
|
465
|
-
export function readUpdateMode() {
|
|
466
|
-
try {
|
|
467
|
-
const m = readFileSync(path.join(selfDir(), 'update-mode'), 'utf8').trim()
|
|
468
|
-
return ['manual', 'auto', 'off'].includes(m) ? m : null
|
|
469
|
-
} catch {
|
|
470
|
-
return null
|
|
471
|
-
}
|
|
472
|
-
}
|
|
1
|
+
// dsh-remote — self-update support.
|
|
2
|
+
//
|
|
3
|
+
// dsh-remote is a zero-build plugin: the host half (lib/index.js) is loaded
|
|
4
|
+
// straight from the installed package, so "updating" = replacing the package
|
|
5
|
+
// files with the newer npm tarball. This module owns the version check and the
|
|
6
|
+
// tarball apply:
|
|
7
|
+
//
|
|
8
|
+
// • checkLatestVersion() — query the npm registry for the `latest` dist-tag;
|
|
9
|
+
// • applyUpdate() — download the tarball, extract package/{lib,package.json,
|
|
10
|
+
// cordis.patch.yml} with a minimal tar reader, verify the version, then
|
|
11
|
+
// replace the installed files atomically (temp file + rename, never a
|
|
12
|
+
// half-written lib/*.js). A `.dsh-remote-updated` marker records the version
|
|
13
|
+
// that landed; the host half removes it once that version is the code it is
|
|
14
|
+
// actually running (see LOADED_VERSION), so the marker means "restart still
|
|
15
|
+
// required" rather than "an update once happened".
|
|
16
|
+
// • reloadSelf() — swap the RUNNING host half to the code now on disk: drop
|
|
17
|
+
// Node's module caches for this package's own files, then dispose + re-init
|
|
18
|
+
// the loader entry so apply() runs again from the fresh module. Without it
|
|
19
|
+
// an update only takes effect on the next process start, which is what left
|
|
20
|
+
// the served client half newer than the loaded host half.
|
|
21
|
+
//
|
|
22
|
+
// Every failure aborts without touching the installed files (a bad download or
|
|
23
|
+
// a version mismatch must never break the running plugin).
|
|
24
|
+
|
|
25
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
|
26
|
+
import { createRequire } from 'node:module'
|
|
27
|
+
import path from 'node:path'
|
|
28
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
29
|
+
|
|
30
|
+
const NPM_PACKAGE = 'dsh-remote'
|
|
31
|
+
const REGISTRY_URL = 'https://registry.npmjs.org/' + NPM_PACKAGE + '/latest'
|
|
32
|
+
|
|
33
|
+
/** Absolute directory holding this plugin's installed files (lib/ + package.json). */
|
|
34
|
+
export function selfDir() {
|
|
35
|
+
try {
|
|
36
|
+
return path.dirname(path.dirname(fileURLToPath(import.meta.url)))
|
|
37
|
+
} catch {
|
|
38
|
+
return path.dirname(path.dirname(process.argv[1] || ''))
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 本插件是否装在 `node_modules` 之下(即真正的"安装副本")。
|
|
44
|
+
*
|
|
45
|
+
* ★ 为什么必须有这道判断(实测依据,不是防御性猜测):
|
|
46
|
+
* `selfDir()` 基于 `import.meta.url`,而 **Node 的 ESM 解析会对符号链接做
|
|
47
|
+
* realpath**(已实测:经由 symlink 导入时 `import.meta.url` 报告的是
|
|
48
|
+
* `/real/mod.mjs` 而不是 `/link/mod.mjs`)。于是当插件以 `link:` / 开发模式
|
|
49
|
+
* 安装时,`selfDir()` 指向的是**用户的源码仓库**,而不是安装副本。
|
|
50
|
+
* 一旦 `auto` 更新落地,`applyUpdate` 就会拿 npm 包覆盖那份源码
|
|
51
|
+
* (丢改动、脏工作树)。默认改成 `auto` 之后这个风险从"手点才触发"
|
|
52
|
+
* 变成"自动发生",所以在此拦下:不在 node_modules 下就不自更新。
|
|
53
|
+
*
|
|
54
|
+
* 代价:少数把插件放在非标准路径的部署不会被自动更新——他们仍可手动更新。
|
|
55
|
+
* 这个方向的取舍是正确的:宁可少更新,也不能悄悄毁掉源码。
|
|
56
|
+
* @param dir - 要判定的目录(默认本插件的 selfDir())。
|
|
57
|
+
*/
|
|
58
|
+
export function isInstalledCopy(dir = selfDir()) {
|
|
59
|
+
// 同时认 `/` 与 `\`:本函数要对两种平台的路径都给出正确答案,而调用方
|
|
60
|
+
// (尤其测试)会传另一种分隔符的路径。先前只按 path.sep 切分,导致
|
|
61
|
+
// 在 Linux 上判断 Windows 风格路径时误判为"非安装副本"(CI 直接红了)。
|
|
62
|
+
const normalized = String(dir).replace(/\\/g, '/').toLowerCase()
|
|
63
|
+
return normalized.includes('/node_modules/')
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Read the installed package.json version; "0.0.0" when unreadable. */
|
|
67
|
+
export function readVersion() {
|
|
68
|
+
try {
|
|
69
|
+
const pkg = JSON.parse(readFileSync(path.join(selfDir(), 'package.json'), 'utf8'))
|
|
70
|
+
return String(pkg.version || '0.0.0')
|
|
71
|
+
} catch {
|
|
72
|
+
return '0.0.0'
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Version of the code this process actually loaded (snapshot at import time).
|
|
77
|
+
* `readVersion()` reads package.json from disk, so after an update lands it
|
|
78
|
+
* reports the NEW version while the running module is still the old one —
|
|
79
|
+
* comparing the two is the only way to tell "applied" from "active". */
|
|
80
|
+
export const LOADED_VERSION = readVersion()
|
|
81
|
+
|
|
82
|
+
/** Package version currently on disk (may be newer than {@link LOADED_VERSION}). */
|
|
83
|
+
export function diskVersion() {
|
|
84
|
+
return readVersion()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Compare dotted versions; returns true when a > b. */
|
|
88
|
+
export function gtVersion(a, b) {
|
|
89
|
+
const pa = String(a || '').split('.').map((n) => parseInt(n, 10) || 0)
|
|
90
|
+
const pb = String(b || '').split('.').map((n) => parseInt(n, 10) || 0)
|
|
91
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
92
|
+
const da = pa[i] || 0
|
|
93
|
+
const db = pb[i] || 0
|
|
94
|
+
if (da !== db) return da > db
|
|
95
|
+
}
|
|
96
|
+
return false
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 查询 npm registry 的最新版本。
|
|
101
|
+
*
|
|
102
|
+
* 返回 `{ version }` 或 `{ error }`(**不再把一切失败压成 null**)。
|
|
103
|
+
*
|
|
104
|
+
* 为什么要区分失败原因:原先任何失败都返回 `null`,调用方只能笼统报
|
|
105
|
+
* "无法连接 npm registry"。实测踩过:本机被 npm 限流(`/latest` 返回 **429**,
|
|
106
|
+
* 而同一时刻 registry 根路径是 200、CI 从另一个出口 IP 也能正常取到),
|
|
107
|
+
* 却被显示成"连不上"——把"等一下就好"误导成"网络坏了"。429/超时/离线
|
|
108
|
+
* 三者的处置完全不同,必须让用户看到真实原因。
|
|
109
|
+
* @param timeoutMs - 超时(看门狗只负责中止,不影响进程存活)。
|
|
110
|
+
*/
|
|
111
|
+
export async function fetchLatestVersion(timeoutMs = 8000) {
|
|
112
|
+
try {
|
|
113
|
+
const controller = new AbortController()
|
|
114
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs)
|
|
115
|
+
// 纯看门狗:调用方并不 await 它,所以必须 unref,否则这一段超时窗口
|
|
116
|
+
// 会拖住事件循环、让本该退出的短命进程(脚本、测试)多挂数秒。
|
|
117
|
+
if (typeof timer.unref === 'function') timer.unref()
|
|
118
|
+
let res
|
|
119
|
+
try {
|
|
120
|
+
res = await fetch(REGISTRY_URL, { signal: controller.signal, headers: { accept: 'application/json' } })
|
|
121
|
+
} finally {
|
|
122
|
+
clearTimeout(timer)
|
|
123
|
+
}
|
|
124
|
+
if (res.status === 429) {
|
|
125
|
+
return { error: 'npm registry 限流(HTTP 429)——请求过于频繁,稍后会自动恢复', reason: 'rate-limited', status: 429 }
|
|
126
|
+
}
|
|
127
|
+
if (!res.ok) {
|
|
128
|
+
return { error: `npm registry 返回 HTTP ${res.status}`, reason: 'http', status: res.status }
|
|
129
|
+
}
|
|
130
|
+
const data = await res.json().catch(() => null)
|
|
131
|
+
const version = typeof data?.version === 'string' && data.version ? data.version : null
|
|
132
|
+
if (!version) return { error: 'npm registry 响应里没有可用的版本号', reason: 'malformed' }
|
|
133
|
+
return { version }
|
|
134
|
+
} catch (err) {
|
|
135
|
+
const aborted = err?.name === 'AbortError'
|
|
136
|
+
return {
|
|
137
|
+
error: aborted ? `连接 npm registry 超时(${timeoutMs}ms)` : '无法连接 npm registry',
|
|
138
|
+
reason: aborted ? 'timeout' : 'network',
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Minimal POSIX ustar reader (regular files only) — npm tarballs are gzipped tar. */
|
|
144
|
+
function parseTar(buf) {
|
|
145
|
+
const files = []
|
|
146
|
+
let off = 0
|
|
147
|
+
while (off + 512 <= buf.length) {
|
|
148
|
+
const header = buf.subarray(off, off + 512)
|
|
149
|
+
const name = header.subarray(0, 100).toString('utf8').replace(/\0.*$/, '')
|
|
150
|
+
if (!name) break
|
|
151
|
+
const sizeStr = header.subarray(124, 136).toString('utf8').replace(/\0.*$/, '').trim()
|
|
152
|
+
const size = parseInt(sizeStr, 8) || 0
|
|
153
|
+
const type = String.fromCharCode(header[156] || 48)
|
|
154
|
+
off += 512
|
|
155
|
+
const data = type === '48' || type === '0' ? Buffer.from(buf.subarray(off, off + size)) : null
|
|
156
|
+
if (data) files.push({ name, data })
|
|
157
|
+
off += Math.ceil(size / 512) * 512
|
|
158
|
+
}
|
|
159
|
+
return files
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Download the given version's tarball and atomically replace the installed
|
|
164
|
+
* package files (lib/*.js, cordis.patch.yml, package.json). On success writes
|
|
165
|
+
* `.dsh-remote-updated` in the install dir; throws on any failure (leaves the
|
|
166
|
+
* installed files untouched).
|
|
167
|
+
* @param targetVersion - exact npm version to install (e.g. "0.8.0").
|
|
168
|
+
*/
|
|
169
|
+
export async function applyUpdate(targetVersion, options = {}) {
|
|
170
|
+
// 拒绝对非安装副本自更新:`auto` 默认开启后,一次自动更新就会覆盖
|
|
171
|
+
// `selfDir()` 指向的目录,而以 link:/开发模式安装时那正是用户的源码仓库
|
|
172
|
+
// (见 isInstalledCopy 的实测说明)。测试用 options.dir 指向临时目录,
|
|
173
|
+
// 因此不受此限(options.dir 存在即视为显式指定,跳过检查)。
|
|
174
|
+
if (!options.dir && !isInstalledCopy()) {
|
|
175
|
+
throw new Error('refusing to self-update: this copy is not under node_modules (link/dev install) — update it from its source repo instead')
|
|
176
|
+
}
|
|
177
|
+
const tarballUrl = `https://registry.npmjs.org/${NPM_PACKAGE}/-/${NPM_PACKAGE}-${targetVersion}.tgz`
|
|
178
|
+
const dir = options.dir || selfDir()
|
|
179
|
+
const doFetch = options.fetchImpl || fetch
|
|
180
|
+
const tmpRoot = path.join(dir, `.dsh-remote-update-${Date.now()}`)
|
|
181
|
+
const tmpPkg = path.join(tmpRoot, 'package')
|
|
182
|
+
try {
|
|
183
|
+
const controller = new AbortController()
|
|
184
|
+
const timer = setTimeout(() => controller.abort(), 60000)
|
|
185
|
+
let res
|
|
186
|
+
try {
|
|
187
|
+
res = await doFetch(tarballUrl, { signal: controller.signal })
|
|
188
|
+
} finally {
|
|
189
|
+
clearTimeout(timer)
|
|
190
|
+
}
|
|
191
|
+
if (!res.ok) throw new Error(`download failed (HTTP ${res.status})`)
|
|
192
|
+
const buf = Buffer.from(await res.arrayBuffer())
|
|
193
|
+
// npm tarballs are gzipped tar; extract the package/ directory.
|
|
194
|
+
const { gunzipSync } = await import('node:zlib')
|
|
195
|
+
const tar = gunzipSync(buf)
|
|
196
|
+
const files = parseTar(tar)
|
|
197
|
+
mkdirSync(tmpPkg, { recursive: true })
|
|
198
|
+
let extracted = 0
|
|
199
|
+
for (const f of files) {
|
|
200
|
+
const rel = f.name.replace(/^package\//, '')
|
|
201
|
+
if (!rel || rel === 'package/' || rel.endsWith('/')) continue
|
|
202
|
+
if (rel !== 'package.json' && rel !== 'cordis.patch.yml' && !rel.startsWith('lib/')) continue
|
|
203
|
+
const dest = path.join(tmpPkg, rel)
|
|
204
|
+
mkdirSync(path.dirname(dest), { recursive: true })
|
|
205
|
+
writeFileSync(dest, f.data)
|
|
206
|
+
extracted++
|
|
207
|
+
}
|
|
208
|
+
if (extracted === 0) throw new Error('tarball contained no package files')
|
|
209
|
+
// Verify the downloaded package.json matches the requested version.
|
|
210
|
+
const newPkg = JSON.parse(readFileSync(path.join(tmpPkg, 'package.json'), 'utf8'))
|
|
211
|
+
if (newPkg.version !== targetVersion) throw new Error(`tarball version mismatch: ${newPkg.version}`)
|
|
212
|
+
// Sanity: the new host half must at least parse (a corrupt index.js would
|
|
213
|
+
// break the next boot — check the file size and that it is not empty).
|
|
214
|
+
const newIndex = path.join(tmpPkg, 'lib', 'index.js')
|
|
215
|
+
if (!existsSync(newIndex) || statSize(newIndex) < 100) throw new Error('tarball missing lib/index.js')
|
|
216
|
+
// Swap the installed files one at a time: atomic, and idempotent.
|
|
217
|
+
//
|
|
218
|
+
// Atomicity (temp file + rename) is not cosmetic here — since the browser
|
|
219
|
+
// half is hot-swapped by dsh-client-hmr, which stat-polls lib/client.js
|
|
220
|
+
// every 500 ms, a torn write would be re-hashed and served mid-copy.
|
|
221
|
+
// Skipping byte-identical files keeps a re-apply of the same version from
|
|
222
|
+
// churning mtimes (and the host half from reloading for nothing).
|
|
223
|
+
const installLib = path.join(dir, 'lib')
|
|
224
|
+
const tmpLib = path.join(tmpPkg, 'lib')
|
|
225
|
+
const changed = []
|
|
226
|
+
const install = (src, dest) => {
|
|
227
|
+
const bytes = readFileSync(src)
|
|
228
|
+
if (sameFileContent(dest, bytes)) return
|
|
229
|
+
try { mkdirSync(path.dirname(dest), { recursive: true }) } catch {}
|
|
230
|
+
writeFileAtomic(dest, bytes)
|
|
231
|
+
changed.push(path.relative(dir, dest).split(path.sep).join('/'))
|
|
232
|
+
}
|
|
233
|
+
for (const name of readdirSafe(tmpLib)) {
|
|
234
|
+
if (!name.endsWith('.js')) continue
|
|
235
|
+
const src = path.join(tmpLib, name)
|
|
236
|
+
if (existsSync(src)) install(src, path.join(installLib, name))
|
|
237
|
+
}
|
|
238
|
+
for (const rel of ['package.json', 'cordis.patch.yml']) {
|
|
239
|
+
const src = path.join(tmpPkg, rel)
|
|
240
|
+
if (existsSync(src)) install(src, path.join(dir, rel))
|
|
241
|
+
}
|
|
242
|
+
writeFileSync(path.join(dir, '.dsh-remote-updated'), String(targetVersion))
|
|
243
|
+
return { ok: true, to: targetVersion, changed }
|
|
244
|
+
} catch (err) {
|
|
245
|
+
throw new Error('update failed: ' + ((err && err.message) || err))
|
|
246
|
+
} finally {
|
|
247
|
+
try { rmSync(tmpRoot, { recursive: true, force: true }) } catch {}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** File size helper (missing/unreadable → 0). */
|
|
252
|
+
function statSize(p) {
|
|
253
|
+
try {
|
|
254
|
+
return statSync(p).size
|
|
255
|
+
} catch {
|
|
256
|
+
return 0
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** readdir helper that returns [] on failure. */
|
|
261
|
+
function readdirSafe(dir) {
|
|
262
|
+
try {
|
|
263
|
+
return readdirSync(dir)
|
|
264
|
+
} catch {
|
|
265
|
+
return []
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Write `data` to `dest` atomically: the bytes land in a sibling temp file and
|
|
271
|
+
* are then renamed over the target, so a concurrent reader (the client-half HMR
|
|
272
|
+
* poll, or the next boot) never observes a truncated file. Falls back to an
|
|
273
|
+
* in-place copy if the platform refuses the replace, and never leaves the temp
|
|
274
|
+
* file behind.
|
|
275
|
+
* @param dest - absolute destination path.
|
|
276
|
+
* @param data - bytes to write.
|
|
277
|
+
*/
|
|
278
|
+
export function writeFileAtomic(dest, data) {
|
|
279
|
+
const tmp = `${dest}.dsh-tmp-${process.pid}-${Date.now().toString(36)}`
|
|
280
|
+
writeFileSync(tmp, data)
|
|
281
|
+
try {
|
|
282
|
+
renameSync(tmp, dest)
|
|
283
|
+
return
|
|
284
|
+
} catch {
|
|
285
|
+
try {
|
|
286
|
+
copyFileSync(tmp, dest)
|
|
287
|
+
} finally {
|
|
288
|
+
try { rmSync(tmp, { force: true }) } catch {}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** Whether the file at `p` already holds exactly `bytes` (missing/short → false). */
|
|
294
|
+
function sameFileContent(p, bytes) {
|
|
295
|
+
try {
|
|
296
|
+
return readFileSync(p).equals(bytes)
|
|
297
|
+
} catch {
|
|
298
|
+
return false
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ── hot swap of the running host half ─────────────────────────────────────
|
|
303
|
+
|
|
304
|
+
/** Loader entry ids this package can own. */
|
|
305
|
+
const SELF_ENTRY_NAMES = ['dsh-remote']
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Find our own loader entry (by id, then by module specifier). Returns undefined
|
|
309
|
+
* when the tree does not contain us (e.g. a test loader, or a renamed row).
|
|
310
|
+
* @param loader - the cordis Loader service (`ctx.loader`).
|
|
311
|
+
*/
|
|
312
|
+
function findSelfEntry(loader) {
|
|
313
|
+
let entries = []
|
|
314
|
+
try {
|
|
315
|
+
entries = [...(loader?.entries?.() ?? [])]
|
|
316
|
+
} catch {
|
|
317
|
+
return undefined
|
|
318
|
+
}
|
|
319
|
+
for (const want of SELF_ENTRY_NAMES) {
|
|
320
|
+
const byId = entries.find((entry) => entry?.options?.id === want)
|
|
321
|
+
if (byId) return byId
|
|
322
|
+
}
|
|
323
|
+
for (const want of SELF_ENTRY_NAMES) {
|
|
324
|
+
const byName = entries.find((entry) => entry?.options?.name === want)
|
|
325
|
+
if (byName) return byName
|
|
326
|
+
}
|
|
327
|
+
return undefined
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Drop Node's module caches for this package's own files, so the next import of
|
|
332
|
+
* the plugin actually re-reads disk instead of handing back the cached module.
|
|
333
|
+
* Both caches matter: the ESM `loadCache` (a plain Map on Node 22/23, a
|
|
334
|
+
* `LoadCache extends Map` on Node 24 — hence the explicit `Map.prototype` calls)
|
|
335
|
+
* and, for CJS modules pulled in through `import()`, `require.cache`.
|
|
336
|
+
* @param loader - the cordis Loader service.
|
|
337
|
+
* @param dir - package directory whose `lib/` to evict (defaults to this
|
|
338
|
+
* package's own install dir; overridable so the swap can be exercised against
|
|
339
|
+
* a fixture install).
|
|
340
|
+
* @returns how many ESM entries were dropped.
|
|
341
|
+
*/
|
|
342
|
+
export function clearSelfModuleCache(loader, dir = selfDir()) {
|
|
343
|
+
const libDir = path.join(dir, 'lib')
|
|
344
|
+
const loadCache = loader?.internal?.loadCache
|
|
345
|
+
const req = (() => {
|
|
346
|
+
try { return createRequire(import.meta.url) } catch { return undefined }
|
|
347
|
+
})()
|
|
348
|
+
let cleared = 0
|
|
349
|
+
for (const name of readdirSafe(libDir)) {
|
|
350
|
+
if (!name.endsWith('.js')) continue
|
|
351
|
+
const url = pathToFileURL(path.join(libDir, name)).href
|
|
352
|
+
try {
|
|
353
|
+
if (loadCache && Map.prototype.has.call(loadCache, url)) {
|
|
354
|
+
Map.prototype.delete.call(loadCache, url)
|
|
355
|
+
cleared += 1
|
|
356
|
+
}
|
|
357
|
+
} catch {}
|
|
358
|
+
try {
|
|
359
|
+
const file = fileURLToPath(url)
|
|
360
|
+
if (req?.cache && req.cache[file]) delete req.cache[file]
|
|
361
|
+
} catch {}
|
|
362
|
+
}
|
|
363
|
+
return cleared
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Swap the RUNNING host half to the code now on disk: clear the module caches
|
|
368
|
+
* for our own files, dispose our loader entry (tools, JSON routes, SSH pools and
|
|
369
|
+
* every `ctx.effect` disposer go with the fiber), then re-init it so `apply()`
|
|
370
|
+
* runs again from the freshly imported module.
|
|
371
|
+
*
|
|
372
|
+
* Callers must not depend on this plugin after a successful reload — the fiber
|
|
373
|
+
* that served the current request is gone. Schedule it (see
|
|
374
|
+
* {@link scheduleSelfReload}) rather than awaiting it from a request handler.
|
|
375
|
+
* @param loader - the cordis Loader service (`ctx.loader`).
|
|
376
|
+
* @returns a small result object; never throws.
|
|
377
|
+
*/
|
|
378
|
+
export async function reloadSelf(loader) {
|
|
379
|
+
const entry = findSelfEntry(loader)
|
|
380
|
+
if (!entry) return { ok: false, reason: 'own loader entry not found' }
|
|
381
|
+
try {
|
|
382
|
+
const cleared = clearSelfModuleCache(loader)
|
|
383
|
+
// ★ 卸载必须走 entry.fiber.dispose()。
|
|
384
|
+
//
|
|
385
|
+
// 原实现找 `entry._dispose` / `entry.dispose`,但 cordis 的 Entry **两个都没有**
|
|
386
|
+
// (0.2.0 实测方法集只有 constructor/disabledOf/evaluate/_patchContext/refresh/
|
|
387
|
+
// update/_commitVolatile/init/_init),所以这里每次都直接返回
|
|
388
|
+
// "entry exposes no dispose hook" —— 热切换从未真正执行过,而
|
|
389
|
+
// scheduleSelfReload 里的 `.catch(() => {})` 把它彻底吞掉,
|
|
390
|
+
// UI 于是永远停在"已落盘但未切换"(用户报的 0.8.35 -> 0.8.36)。
|
|
391
|
+
//
|
|
392
|
+
// 也不能用 entry.refresh():它是 `if (this.fiber) return; await this.init()`,
|
|
393
|
+
// fiber 还活着时直接返回,等于没重启。真正卸载 fiber 的地方在 Entry.update()
|
|
394
|
+
// 里(`this.fiber?.dispose()`),这里照它做。
|
|
395
|
+
//
|
|
396
|
+
// 保留对 _dispose/dispose 的探测:那是宿主可能提供的扩展钩子,有则优先用,
|
|
397
|
+
// 没有就用 fiber.dispose()——两条路都必须能真正卸载,否则应如实报错。
|
|
398
|
+
// DSH 0.2 的 loader 在 fiber 被卸载时把 entry.options.disabled 写成 true
|
|
399
|
+
// 并写回配置树。客户端扫描会跳过禁用行,于是热切换之后宿主能起来、
|
|
400
|
+
// 设置页的 client 不再下发。这里记下卸载前的状态:只有 loader 在
|
|
401
|
+
// dispose 期间新打上的禁用才清掉。用户本来就禁用的行不要重新打开。
|
|
402
|
+
const disabledBefore = entry.options?.disabled === true
|
|
403
|
+
if (typeof entry._dispose === 'function') await entry._dispose()
|
|
404
|
+
else if (typeof entry.dispose === 'function') await entry.dispose()
|
|
405
|
+
else if (entry.fiber && typeof entry.fiber.dispose === 'function') await entry.fiber.dispose()
|
|
406
|
+
else return { ok: false, reason: 'entry exposes no dispose hook' }
|
|
407
|
+
if (!disabledBefore && entry.options?.disabled === true) {
|
|
408
|
+
delete entry.options.disabled
|
|
409
|
+
entry.parent?.tree?.write?.()
|
|
410
|
+
}
|
|
411
|
+
await entry.init()
|
|
412
|
+
return { ok: true, cleared }
|
|
413
|
+
} catch (err) {
|
|
414
|
+
return { ok: false, reason: String((err && err.message) || err) }
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Fire-and-forget {@link reloadSelf} after a short delay, so the caller can
|
|
420
|
+
* finish answering an HTTP request (or an update check) before its own module is
|
|
421
|
+
* torn down. A failed swap leaves the running code in place, which is the
|
|
422
|
+
* pre-hot-update behaviour, not a crash — so we do not throw.
|
|
423
|
+
*
|
|
424
|
+
* We DO record the outcome, though. Silently discarding it is precisely how the
|
|
425
|
+
* "entry exposes no dispose hook" defect stayed invisible: the panel reported a
|
|
426
|
+
* scheduled swap, the swap never ran, and nothing anywhere said so.
|
|
427
|
+
* {@link lastSelfReload} exposes the result for diagnostics (the settings panel
|
|
428
|
+
* and `/dsh-remote/update-check` surface it), and a non-ok result is logged once.
|
|
429
|
+
*
|
|
430
|
+
* @param loader - the cordis Loader service.
|
|
431
|
+
* @param delayMs - grace period before the swap (default 300 ms).
|
|
432
|
+
* @returns true when the swap was scheduled.
|
|
433
|
+
*/
|
|
434
|
+
export function scheduleSelfReload(loader, delayMs = 300) {
|
|
435
|
+
const timer = setTimeout(() => {
|
|
436
|
+
void reloadSelf(loader).then(
|
|
437
|
+
(res) => { lastReloadResult = res; if (res && res.ok === false) console.warn('[dsh-remote] self-update swap failed:', res.reason) },
|
|
438
|
+
(err) => { lastReloadResult = { ok: false, reason: String((err && err.message) || err) }; console.warn('[dsh-remote] self-update swap threw:', lastReloadResult.reason) },
|
|
439
|
+
)
|
|
440
|
+
}, Math.max(0, delayMs))
|
|
441
|
+
if (typeof timer.unref === 'function') timer.unref()
|
|
442
|
+
return true
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/** Result of the most recent {@link scheduleSelfReload} attempt, for diagnostics. */
|
|
446
|
+
let lastReloadResult = null
|
|
447
|
+
|
|
448
|
+
/** @returns the last self-reload outcome (`{ok,reason?}`, or null if never attempted). */
|
|
449
|
+
export function lastSelfReload() {
|
|
450
|
+
return lastReloadResult
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/** Write the persisted update-mode override (settings UI). */
|
|
454
|
+
export function persistUpdateMode(mode) {
|
|
455
|
+
if (!['manual', 'auto', 'off'].includes(mode)) return false
|
|
456
|
+
try {
|
|
457
|
+
writeFileSync(path.join(selfDir(), 'update-mode'), mode)
|
|
458
|
+
return true
|
|
459
|
+
} catch {
|
|
460
|
+
return false
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/** Read a persisted update-mode override, or null when absent/invalid. */
|
|
465
|
+
export function readUpdateMode() {
|
|
466
|
+
try {
|
|
467
|
+
const m = readFileSync(path.join(selfDir(), 'update-mode'), 'utf8').trim()
|
|
468
|
+
return ['manual', 'auto', 'off'].includes(m) ? m : null
|
|
469
|
+
} catch {
|
|
470
|
+
return null
|
|
471
|
+
}
|
|
472
|
+
}
|