@marktrue/dsh-mobile-remote 3.2.2-alpha.1-app → 3.2.2-alpha.1
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/CHANGELOG.md +1223 -0
- package/CONTRIBUTING.md +59 -0
- package/FAQ.md +111 -0
- package/README.md +253 -6
- package/cordis.patch.yml +14 -0
- package/docs/00-/345/274/200/345/217/221/346/200/273/347/272/262.md +125 -0
- package/docs/01-PRD.md +148 -0
- package/docs/02-architecture.md +249 -0
- package/docs/03-api.md +930 -0
- package/docs/04-security.md +100 -0
- package/docs/05-test-cases.md +366 -0
- package/docs/06-install-run.md +372 -0
- package/docs/07-user-manual.md +66 -0
- package/docs/08-/346/211/251/345/261/225/345/274/200/345/217/221/346/214/207/345/215/227.md +86 -0
- package/docs/09-compatibility.md +223 -0
- package/docs/10-release.md +929 -0
- package/docs/screenshots/01-home.png +0 -0
- package/docs/screenshots/02-notifications.png +0 -0
- package/docs/screenshots/03-sessions.png +0 -0
- package/docs/screenshots/04-chat.png +0 -0
- package/docs/screenshots/05-settings.png +0 -0
- package/docs/screenshots/06-new-session.png +0 -0
- package/docs/screenshots/pgy-appstore.jpg +0 -0
- package/docs/screenshots/pgy-mobile-app.jpg +0 -0
- package/docs/screenshots/pgy-pc-client.jpg +0 -0
- package/docs/spec-app-auto-update.md +143 -0
- package/lib/account-usage.js +145 -0
- package/lib/app-alignment.js +112 -0
- package/lib/app-carrier-store.js +1972 -0
- package/lib/app-carrier.js +609 -0
- package/lib/client.js +1174 -0
- package/lib/desktop-form.js +738 -0
- package/lib/endpoint-registry.js +165 -0
- package/lib/git-read-bundled-provider.js +255 -0
- package/lib/git-read-service.js +322 -0
- package/lib/index.js +9467 -0
- package/lib/npm-update-source.js +226 -0
- package/lib/plugin-content-digest.js +80 -0
- package/lib/plugin-self-update.js +997 -0
- package/lib/plugin-update-controller.js +274 -0
- package/lib/plugin-update-log.js +164 -0
- package/lib/plugin-update-transaction.js +322 -0
- package/lib/plugin-update.js +309 -0
- package/lib/runtime-dependency-snapshot.js +345 -0
- package/lib/session-title-cache.js +123 -0
- package/lib/session-title-refresh.js +261 -0
- package/lib/tool-process.js +20 -0
- package/lib/update-fetch.js +24 -0
- package/lib/update-lock-arbiter.js +170 -0
- package/lib/update-supervisor.js +346 -0
- package/node_modules/ansi-regex/index.d.ts +37 -0
- package/node_modules/ansi-regex/index.js +10 -0
- package/node_modules/ansi-regex/license +9 -0
- package/node_modules/ansi-regex/package.json +55 -0
- package/node_modules/ansi-regex/readme.md +78 -0
- package/node_modules/ansi-styles/index.d.ts +345 -0
- package/node_modules/ansi-styles/index.js +163 -0
- package/node_modules/ansi-styles/license +9 -0
- package/node_modules/ansi-styles/package.json +56 -0
- package/node_modules/ansi-styles/readme.md +152 -0
- package/node_modules/camelcase/index.d.ts +63 -0
- package/node_modules/camelcase/index.js +76 -0
- package/node_modules/camelcase/license +9 -0
- package/node_modules/camelcase/package.json +43 -0
- package/node_modules/camelcase/readme.md +99 -0
- package/node_modules/cliui/CHANGELOG.md +76 -0
- package/node_modules/cliui/LICENSE.txt +14 -0
- package/node_modules/cliui/README.md +115 -0
- package/node_modules/cliui/index.js +354 -0
- package/node_modules/cliui/package.json +65 -0
- package/node_modules/color-convert/CHANGELOG.md +54 -0
- package/node_modules/color-convert/LICENSE +21 -0
- package/node_modules/color-convert/README.md +68 -0
- package/node_modules/color-convert/conversions.js +839 -0
- package/node_modules/color-convert/index.js +81 -0
- package/node_modules/color-convert/package.json +48 -0
- package/node_modules/color-convert/route.js +97 -0
- package/node_modules/color-name/LICENSE +8 -0
- package/node_modules/color-name/README.md +11 -0
- package/node_modules/color-name/index.js +152 -0
- package/node_modules/color-name/package.json +28 -0
- package/node_modules/decamelize/index.js +13 -0
- package/node_modules/decamelize/license +21 -0
- package/node_modules/decamelize/package.json +38 -0
- package/node_modules/decamelize/readme.md +48 -0
- package/node_modules/dijkstrajs/.travis.yml +4 -0
- package/node_modules/dijkstrajs/CONTRIBUTING.md +8 -0
- package/node_modules/dijkstrajs/LICENSE.md +19 -0
- package/node_modules/dijkstrajs/README.md +18 -0
- package/node_modules/dijkstrajs/dijkstra.js +165 -0
- package/node_modules/dijkstrajs/package.json +60 -0
- package/node_modules/dijkstrajs/test/dijkstra.test.js +96 -0
- package/node_modules/emoji-regex/LICENSE-MIT.txt +20 -0
- package/node_modules/emoji-regex/README.md +73 -0
- package/node_modules/emoji-regex/es2015/index.js +6 -0
- package/node_modules/emoji-regex/es2015/text.js +6 -0
- package/node_modules/emoji-regex/index.d.ts +23 -0
- package/node_modules/emoji-regex/index.js +6 -0
- package/node_modules/emoji-regex/package.json +50 -0
- package/node_modules/emoji-regex/text.js +6 -0
- package/node_modules/find-up/index.d.ts +137 -0
- package/node_modules/find-up/index.js +89 -0
- package/node_modules/find-up/license +9 -0
- package/node_modules/find-up/package.json +53 -0
- package/node_modules/find-up/readme.md +156 -0
- package/node_modules/get-caller-file/LICENSE.md +6 -0
- package/node_modules/get-caller-file/README.md +41 -0
- package/node_modules/get-caller-file/index.d.ts +2 -0
- package/node_modules/get-caller-file/index.js +22 -0
- package/node_modules/get-caller-file/index.js.map +1 -0
- package/node_modules/get-caller-file/package.json +42 -0
- package/node_modules/is-fullwidth-code-point/index.d.ts +17 -0
- package/node_modules/is-fullwidth-code-point/index.js +50 -0
- package/node_modules/is-fullwidth-code-point/license +9 -0
- package/node_modules/is-fullwidth-code-point/package.json +42 -0
- package/node_modules/is-fullwidth-code-point/readme.md +39 -0
- package/node_modules/locate-path/index.d.ts +83 -0
- package/node_modules/locate-path/index.js +65 -0
- package/node_modules/locate-path/license +9 -0
- package/node_modules/locate-path/package.json +45 -0
- package/node_modules/locate-path/readme.md +122 -0
- package/node_modules/p-limit/index.d.ts +38 -0
- package/node_modules/p-limit/index.js +57 -0
- package/node_modules/p-limit/license +9 -0
- package/node_modules/p-limit/package.json +52 -0
- package/node_modules/p-limit/readme.md +101 -0
- package/node_modules/p-locate/index.d.ts +64 -0
- package/node_modules/p-locate/index.js +52 -0
- package/node_modules/p-locate/license +9 -0
- package/node_modules/p-locate/package.json +53 -0
- package/node_modules/p-locate/readme.md +90 -0
- package/node_modules/p-try/index.d.ts +39 -0
- package/node_modules/p-try/index.js +9 -0
- package/node_modules/p-try/license +9 -0
- package/node_modules/p-try/package.json +42 -0
- package/node_modules/p-try/readme.md +58 -0
- package/node_modules/path-exists/index.d.ts +28 -0
- package/node_modules/path-exists/index.js +23 -0
- package/node_modules/path-exists/license +9 -0
- package/node_modules/path-exists/package.json +39 -0
- package/node_modules/path-exists/readme.md +52 -0
- package/node_modules/pngjs/.eslintignore +2 -0
- package/node_modules/pngjs/.eslintrc.json +19 -0
- package/node_modules/pngjs/.prettierignore +3 -0
- package/node_modules/pngjs/LICENSE +20 -0
- package/node_modules/pngjs/README.md +429 -0
- package/node_modules/pngjs/browser.js +16096 -0
- package/node_modules/pngjs/coverage/lcov-report/base.css +354 -0
- package/node_modules/pngjs/coverage/lcov-report/bitmapper.js.html +881 -0
- package/node_modules/pngjs/coverage/lcov-report/bitpacker.js.html +554 -0
- package/node_modules/pngjs/coverage/lcov-report/block-navigation.js +77 -0
- package/node_modules/pngjs/coverage/lcov-report/chunkstream.js.html +647 -0
- package/node_modules/pngjs/coverage/lcov-report/constants.js.html +176 -0
- package/node_modules/pngjs/coverage/lcov-report/crc.js.html +200 -0
- package/node_modules/pngjs/coverage/lcov-report/favicon.png +0 -0
- package/node_modules/pngjs/coverage/lcov-report/filter-pack.js.html +593 -0
- package/node_modules/pngjs/coverage/lcov-report/filter-parse-async.js.html +152 -0
- package/node_modules/pngjs/coverage/lcov-report/filter-parse-sync.js.html +143 -0
- package/node_modules/pngjs/coverage/lcov-report/filter-parse.js.html +611 -0
- package/node_modules/pngjs/coverage/lcov-report/format-normaliser.js.html +359 -0
- package/node_modules/pngjs/coverage/lcov-report/index.html +426 -0
- package/node_modules/pngjs/coverage/lcov-report/interlace.js.html +365 -0
- package/node_modules/pngjs/coverage/lcov-report/packer-async.js.html +230 -0
- package/node_modules/pngjs/coverage/lcov-report/packer-sync.js.html +248 -0
- package/node_modules/pngjs/coverage/lcov-report/packer.js.html +467 -0
- package/node_modules/pngjs/coverage/lcov-report/paeth-predictor.js.html +128 -0
- package/node_modules/pngjs/coverage/lcov-report/parser-async.js.html +575 -0
- package/node_modules/pngjs/coverage/lcov-report/parser-sync.js.html +404 -0
- package/node_modules/pngjs/coverage/lcov-report/parser.js.html +950 -0
- package/node_modules/pngjs/coverage/lcov-report/png-sync.js.html +116 -0
- package/node_modules/pngjs/coverage/lcov-report/png.js.html +662 -0
- package/node_modules/pngjs/coverage/lcov-report/prettify.css +101 -0
- package/node_modules/pngjs/coverage/lcov-report/prettify.js +1007 -0
- package/node_modules/pngjs/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/node_modules/pngjs/coverage/lcov-report/sorter.js +169 -0
- package/node_modules/pngjs/coverage/lcov-report/sync-inflate.js.html +584 -0
- package/node_modules/pngjs/coverage/lcov-report/sync-reader.js.html +215 -0
- package/node_modules/pngjs/coverage/lcov.info +2177 -0
- package/node_modules/pngjs/lib/bitmapper.js +267 -0
- package/node_modules/pngjs/lib/bitpacker.js +158 -0
- package/node_modules/pngjs/lib/chunkstream.js +189 -0
- package/node_modules/pngjs/lib/constants.js +32 -0
- package/node_modules/pngjs/lib/crc.js +40 -0
- package/node_modules/pngjs/lib/filter-pack.js +171 -0
- package/node_modules/pngjs/lib/filter-parse-async.js +24 -0
- package/node_modules/pngjs/lib/filter-parse-sync.js +21 -0
- package/node_modules/pngjs/lib/filter-parse.js +177 -0
- package/node_modules/pngjs/lib/format-normaliser.js +93 -0
- package/node_modules/pngjs/lib/interlace.js +95 -0
- package/node_modules/pngjs/lib/packer-async.js +50 -0
- package/node_modules/pngjs/lib/packer-sync.js +56 -0
- package/node_modules/pngjs/lib/packer.js +129 -0
- package/node_modules/pngjs/lib/paeth-predictor.js +16 -0
- package/node_modules/pngjs/lib/parser-async.js +165 -0
- package/node_modules/pngjs/lib/parser-sync.js +108 -0
- package/node_modules/pngjs/lib/parser.js +290 -0
- package/node_modules/pngjs/lib/png-sync.js +12 -0
- package/node_modules/pngjs/lib/png.js +194 -0
- package/node_modules/pngjs/lib/sync-inflate.js +168 -0
- package/node_modules/pngjs/lib/sync-reader.js +45 -0
- package/node_modules/pngjs/package.json +73 -0
- package/node_modules/qrcode/README.md +770 -0
- package/node_modules/qrcode/bin/qrcode +159 -0
- package/node_modules/qrcode/helper/to-sjis-browser.js +2 -0
- package/node_modules/qrcode/helper/to-sjis.js +105 -0
- package/node_modules/qrcode/lib/browser.js +76 -0
- package/node_modules/qrcode/lib/can-promise.js +7 -0
- package/node_modules/qrcode/lib/core/alignment-pattern.js +83 -0
- package/node_modules/qrcode/lib/core/alphanumeric-data.js +59 -0
- package/node_modules/qrcode/lib/core/bit-buffer.js +37 -0
- package/node_modules/qrcode/lib/core/bit-matrix.js +65 -0
- package/node_modules/qrcode/lib/core/byte-data.js +30 -0
- package/node_modules/qrcode/lib/core/error-correction-code.js +135 -0
- package/node_modules/qrcode/lib/core/error-correction-level.js +50 -0
- package/node_modules/qrcode/lib/core/finder-pattern.js +22 -0
- package/node_modules/qrcode/lib/core/format-info.js +29 -0
- package/node_modules/qrcode/lib/core/galois-field.js +69 -0
- package/node_modules/qrcode/lib/core/kanji-data.js +54 -0
- package/node_modules/qrcode/lib/core/mask-pattern.js +234 -0
- package/node_modules/qrcode/lib/core/mode.js +167 -0
- package/node_modules/qrcode/lib/core/numeric-data.js +43 -0
- package/node_modules/qrcode/lib/core/polynomial.js +62 -0
- package/node_modules/qrcode/lib/core/qrcode.js +495 -0
- package/node_modules/qrcode/lib/core/reed-solomon-encoder.js +56 -0
- package/node_modules/qrcode/lib/core/regex.js +31 -0
- package/node_modules/qrcode/lib/core/segments.js +330 -0
- package/node_modules/qrcode/lib/core/utils.js +63 -0
- package/node_modules/qrcode/lib/core/version-check.js +9 -0
- package/node_modules/qrcode/lib/core/version.js +163 -0
- package/node_modules/qrcode/lib/index.js +12 -0
- package/node_modules/qrcode/lib/renderer/canvas.js +63 -0
- package/node_modules/qrcode/lib/renderer/png.js +78 -0
- package/node_modules/qrcode/lib/renderer/svg-tag.js +81 -0
- package/node_modules/qrcode/lib/renderer/svg.js +19 -0
- package/node_modules/qrcode/lib/renderer/terminal/terminal-small.js +85 -0
- package/node_modules/qrcode/lib/renderer/terminal/terminal.js +49 -0
- package/node_modules/qrcode/lib/renderer/terminal.js +9 -0
- package/node_modules/qrcode/lib/renderer/utf8.js +71 -0
- package/node_modules/qrcode/lib/renderer/utils.js +99 -0
- package/node_modules/qrcode/lib/server.js +138 -0
- package/node_modules/qrcode/license +10 -0
- package/node_modules/qrcode/package.json +77 -0
- package/node_modules/require-directory/.jshintrc +67 -0
- package/node_modules/require-directory/.npmignore +1 -0
- package/node_modules/require-directory/.travis.yml +3 -0
- package/node_modules/require-directory/LICENSE +22 -0
- package/node_modules/require-directory/README.markdown +184 -0
- package/node_modules/require-directory/index.js +86 -0
- package/node_modules/require-directory/package.json +40 -0
- package/node_modules/require-main-filename/CHANGELOG.md +26 -0
- package/node_modules/require-main-filename/LICENSE.txt +14 -0
- package/node_modules/require-main-filename/README.md +26 -0
- package/node_modules/require-main-filename/index.js +18 -0
- package/node_modules/require-main-filename/package.json +35 -0
- package/node_modules/set-blocking/CHANGELOG.md +26 -0
- package/node_modules/set-blocking/LICENSE.txt +14 -0
- package/node_modules/set-blocking/README.md +31 -0
- package/node_modules/set-blocking/index.js +7 -0
- package/node_modules/set-blocking/package.json +42 -0
- package/node_modules/string-width/index.d.ts +29 -0
- package/node_modules/string-width/index.js +47 -0
- package/node_modules/string-width/license +9 -0
- package/node_modules/string-width/package.json +56 -0
- package/node_modules/string-width/readme.md +50 -0
- package/node_modules/strip-ansi/index.d.ts +17 -0
- package/node_modules/strip-ansi/index.js +4 -0
- package/node_modules/strip-ansi/license +9 -0
- package/node_modules/strip-ansi/package.json +54 -0
- package/node_modules/strip-ansi/readme.md +46 -0
- package/node_modules/which-module/LICENSE +13 -0
- package/node_modules/which-module/README.md +58 -0
- package/node_modules/which-module/index.js +9 -0
- package/node_modules/which-module/package.json +41 -0
- package/node_modules/wrap-ansi/index.js +186 -0
- package/node_modules/wrap-ansi/license +9 -0
- package/node_modules/wrap-ansi/package.json +61 -0
- package/node_modules/wrap-ansi/readme.md +97 -0
- package/node_modules/y18n/CHANGELOG.md +35 -0
- package/node_modules/y18n/LICENSE +13 -0
- package/node_modules/y18n/README.md +109 -0
- package/node_modules/y18n/index.js +188 -0
- package/node_modules/y18n/package.json +39 -0
- package/node_modules/yargs/CHANGELOG.md +420 -0
- package/node_modules/yargs/LICENSE +21 -0
- package/node_modules/yargs/README.md +140 -0
- package/node_modules/yargs/build/lib/apply-extends.d.ts +2 -0
- package/node_modules/yargs/build/lib/apply-extends.js +65 -0
- package/node_modules/yargs/build/lib/argsert.d.ts +2 -0
- package/node_modules/yargs/build/lib/argsert.js +65 -0
- package/node_modules/yargs/build/lib/command.d.ts +64 -0
- package/node_modules/yargs/build/lib/command.js +416 -0
- package/node_modules/yargs/build/lib/common-types.d.ts +36 -0
- package/node_modules/yargs/build/lib/common-types.js +25 -0
- package/node_modules/yargs/build/lib/completion-templates.d.ts +2 -0
- package/node_modules/yargs/build/lib/completion-templates.js +50 -0
- package/node_modules/yargs/build/lib/completion.d.ts +21 -0
- package/node_modules/yargs/build/lib/completion.js +135 -0
- package/node_modules/yargs/build/lib/is-promise.d.ts +1 -0
- package/node_modules/yargs/build/lib/is-promise.js +9 -0
- package/node_modules/yargs/build/lib/levenshtein.d.ts +1 -0
- package/node_modules/yargs/build/lib/levenshtein.js +58 -0
- package/node_modules/yargs/build/lib/middleware.d.ts +10 -0
- package/node_modules/yargs/build/lib/middleware.js +57 -0
- package/node_modules/yargs/build/lib/obj-filter.d.ts +1 -0
- package/node_modules/yargs/build/lib/obj-filter.js +14 -0
- package/node_modules/yargs/build/lib/parse-command.d.ts +11 -0
- package/node_modules/yargs/build/lib/parse-command.js +36 -0
- package/node_modules/yargs/build/lib/process-argv.d.ts +2 -0
- package/node_modules/yargs/build/lib/process-argv.js +31 -0
- package/node_modules/yargs/build/lib/usage.d.ts +49 -0
- package/node_modules/yargs/build/lib/usage.js +540 -0
- package/node_modules/yargs/build/lib/validation.d.ts +34 -0
- package/node_modules/yargs/build/lib/validation.js +330 -0
- package/node_modules/yargs/build/lib/yargs.d.ts +274 -0
- package/node_modules/yargs/build/lib/yargs.js +1190 -0
- package/node_modules/yargs/build/lib/yerror.d.ts +4 -0
- package/node_modules/yargs/build/lib/yerror.js +11 -0
- package/node_modules/yargs/index.js +40 -0
- package/node_modules/yargs/locales/be.json +46 -0
- package/node_modules/yargs/locales/de.json +46 -0
- package/node_modules/yargs/locales/en.json +51 -0
- package/node_modules/yargs/locales/es.json +46 -0
- package/node_modules/yargs/locales/fi.json +49 -0
- package/node_modules/yargs/locales/fr.json +53 -0
- package/node_modules/yargs/locales/hi.json +49 -0
- package/node_modules/yargs/locales/hu.json +46 -0
- package/node_modules/yargs/locales/id.json +50 -0
- package/node_modules/yargs/locales/it.json +46 -0
- package/node_modules/yargs/locales/ja.json +51 -0
- package/node_modules/yargs/locales/ko.json +49 -0
- package/node_modules/yargs/locales/nb.json +44 -0
- package/node_modules/yargs/locales/nl.json +49 -0
- package/node_modules/yargs/locales/nn.json +44 -0
- package/node_modules/yargs/locales/pirate.json +13 -0
- package/node_modules/yargs/locales/pl.json +49 -0
- package/node_modules/yargs/locales/pt.json +45 -0
- package/node_modules/yargs/locales/pt_BR.json +48 -0
- package/node_modules/yargs/locales/ru.json +46 -0
- package/node_modules/yargs/locales/th.json +46 -0
- package/node_modules/yargs/locales/tr.json +48 -0
- package/node_modules/yargs/locales/zh_CN.json +48 -0
- package/node_modules/yargs/locales/zh_TW.json +47 -0
- package/node_modules/yargs/package.json +92 -0
- package/node_modules/yargs/yargs.js +14 -0
- package/node_modules/yargs-parser/CHANGELOG.md +601 -0
- package/node_modules/yargs-parser/LICENSE.txt +14 -0
- package/node_modules/yargs-parser/README.md +449 -0
- package/node_modules/yargs-parser/index.js +1032 -0
- package/node_modules/yargs-parser/lib/tokenize-arg-string.js +40 -0
- package/node_modules/yargs-parser/package.json +46 -0
- package/package.json +65 -12
- package/DSH-Remote-v3.2.2.apk +0 -4
- package/manifest.json +0 -21
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
// 插件自更新的**事务**(ADR 0019「插件自更新」)。
|
|
2
|
+
//
|
|
3
|
+
// 评审(GitLab #32 note 1286)指出的根因是:原来的实现把"回滚"放在**新版插件自己的
|
|
4
|
+
// apply() 里**,而新版插件一旦起不来就永远走不到那里;落位目录又只按版本号命名,
|
|
5
|
+
// 同版本不同内容的更新会先删掉自己的回滚目标;profile 目录还是从模块路径"往上数两层"
|
|
6
|
+
// 猜出来的,在 pnpm 的 virtual store 布局下必错。
|
|
7
|
+
//
|
|
8
|
+
// 本模块把这些前提一次性做对:
|
|
9
|
+
// 1. **不可变产物目录**:按内容标识命名,绝不覆盖既有目录(回滚目标因此永远还在);
|
|
10
|
+
// 2. **单事务 + 锁**:同一 profile 同时只允许一个更新事务,重复请求幂等返回同一 opId;
|
|
11
|
+
// 3. **profile 目录靠"验证"而不是靠层级推断**:向上找第一个声明了本插件依赖的
|
|
12
|
+
// package.json;找不到就返回空,调用方转降级指引,绝不动文件。
|
|
13
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
14
|
+
import { dirname, join, resolve } from "node:path";
|
|
15
|
+
import { freezePackage, frozenPackageReady } from "./runtime-dependency-snapshot.js";
|
|
16
|
+
import { randomUUID } from "node:crypto";
|
|
17
|
+
import { spawnSync } from "node:child_process";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
|
|
20
|
+
/** 事务阶段。`RESTART_PENDING` 是唯一需要外部监督器介入的阶段。 */
|
|
21
|
+
export const TRANSACTION_PHASE = {
|
|
22
|
+
/** 正在下载/解包/落位:还没动 profile,失败可安全放弃。 */
|
|
23
|
+
STAGING: "staging",
|
|
24
|
+
/** 依赖已切换、正在安装。 */
|
|
25
|
+
INSTALLING: "installing",
|
|
26
|
+
/** 已提交,等待宿主重启生效——**监督器接管**。 */
|
|
27
|
+
RESTART_PENDING: "restart-pending",
|
|
28
|
+
/** 正在回滚(**中间态**:App 不得据此判成功,F7)。 */
|
|
29
|
+
ROLLING_BACK: "rolling-back",
|
|
30
|
+
/** 新版本已确认健康并生效。 */
|
|
31
|
+
SETTLED: "settled",
|
|
32
|
+
/** 已回滚到上一版并确认可用。 */
|
|
33
|
+
ROLLED_BACK: "rolled-back",
|
|
34
|
+
/** 失败(含回滚失败)。 */
|
|
35
|
+
FAILED: "failed",
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** 锁的默认失效时间:超过它视为持有者已死(宿主崩溃/被杀)。 */
|
|
39
|
+
export const DEFAULT_LOCK_STALE_MS = 10 * 60 * 1000;
|
|
40
|
+
|
|
41
|
+
export function newOperationId() {
|
|
42
|
+
return randomUUID();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** 事务状态文件(放在插件数据目录,与通知/队列同源)。 */
|
|
46
|
+
export function transactionFile(home) {
|
|
47
|
+
return join(home, ".dsh", "mobile-remote", "plugin-update-transaction.json");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** 更新锁**目录**(mkdir 原子独占;见 acquireLock)。 */
|
|
51
|
+
export function updateLockPath(home) {
|
|
52
|
+
return join(home, ".dsh", "mobile-remote", "plugin-update.lock.d");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** 兼容旧名(它指向的是一个目录,不再是文件)。 */
|
|
56
|
+
export const updateLockFile = updateLockPath;
|
|
57
|
+
|
|
58
|
+
/** 下载/解包的工作目录(事务独占)。 */
|
|
59
|
+
export function workRoot(home) {
|
|
60
|
+
return join(home, ".dsh", "mobile-remote", "update-work");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** 不可变产物目录根。 */
|
|
64
|
+
export function artifactRoot(home) {
|
|
65
|
+
return join(home, ".dsh", "mobile-remote", "plugin-artifacts");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** 不可变快照根(回滚目标)。 */
|
|
69
|
+
export function snapshotRoot(home) {
|
|
70
|
+
return join(home, ".dsh", "mobile-remote", "plugin-snapshots");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** 按**内容标识**命名的不可变产物目录;同版本不同内容因此天然分属不同目录。 */
|
|
74
|
+
export function artifactDir(root, contentSha256) {
|
|
75
|
+
return join(root, String(contentSha256 || "unknown").toLowerCase());
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** 按**内容标识**命名的不可变快照目录(更新前的旧内容)。 */
|
|
79
|
+
export function snapshotDir(root, contentSha256) {
|
|
80
|
+
return join(root, String(contentSha256 || "unknown").toLowerCase());
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function readTransaction(file) {
|
|
84
|
+
try {
|
|
85
|
+
const doc = JSON.parse(readFileSync(file, "utf8"));
|
|
86
|
+
return doc && typeof doc === "object" ? doc : null;
|
|
87
|
+
} catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function writeTransaction(file, tx) {
|
|
93
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
94
|
+
const next = { ...tx, updatedAt: new Date().toISOString() };
|
|
95
|
+
writeFileSync(file, `${JSON.stringify(next, null, 2)}\n`, "utf8");
|
|
96
|
+
return next;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function clearTransaction(file) {
|
|
100
|
+
try {
|
|
101
|
+
rmSync(file, { force: true });
|
|
102
|
+
} catch {
|
|
103
|
+
// 清不掉不影响正确性:下次事务会覆盖它
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Converge only under the same OS arbitration as acquire/release/state writes. */
|
|
108
|
+
export function convergeStaleTransaction({ stateFile, lockPath, expectedLease, now = () => Date.now(), staleMs = 600_000 } = {}) {
|
|
109
|
+
const result = arbitrate(lockPath, { operation: "converge", stateFile, expectedLease, now: now(), staleMs });
|
|
110
|
+
if (!result.ok) return { action: "blocked", error: result.error, transaction: readTransaction(stateFile) };
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 定位 profile 目录:从插件真实路径向上找**第一个声明了本插件依赖**的 package.json。
|
|
116
|
+
*
|
|
117
|
+
* 为什么不"往上数两层":Node 解析包链接时会走 realpath,pnpm 标准布局下插件位于
|
|
118
|
+
* `profile/node_modules/.pnpm/<entry>/node_modules/dsh-mobile-remote`,往上两层得到的是
|
|
119
|
+
* `.pnpm/<entry>` 而不是 profile。这里改成**验证式查找**:找到的目录必须真的把
|
|
120
|
+
* `dsh-mobile-remote` 写在 dependencies 里,否则继续往上,全都不匹配就返回空串。
|
|
121
|
+
*/
|
|
122
|
+
/** 旧键:早期安装用的包名(保持向后兼容)。 */
|
|
123
|
+
export const LEGACY_DEPENDENCY_KEY = "dsh-mobile-remote";
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 本插件在 profile 里可能出现的依赖键。
|
|
127
|
+
* 包名一旦带 scope(npm 正式渠道),profile 的依赖键就变成 `@scope/name`——
|
|
128
|
+
* 若还硬编码旧键,自更新会找不到 profile。因此键从**插件自己的 package.json** 推导。
|
|
129
|
+
*/
|
|
130
|
+
export function dependencyKeysFor(pluginRoot, { readJson = readJsonFile } = {}) {
|
|
131
|
+
const keys = [];
|
|
132
|
+
try {
|
|
133
|
+
const name = readJson(join(pluginRoot, "package.json"))?.name;
|
|
134
|
+
if (typeof name === "string" && name.trim() !== "") keys.push(name.trim());
|
|
135
|
+
} catch {
|
|
136
|
+
// 读不到自己的包名时只留旧键
|
|
137
|
+
}
|
|
138
|
+
if (!keys.includes(LEGACY_DEPENDENCY_KEY)) keys.push(LEGACY_DEPENDENCY_KEY);
|
|
139
|
+
return keys;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** 在 profile 的 dependencies 里找出实际使用的那个依赖键(找不到返回空串)。 */
|
|
143
|
+
export function dependencyKeyIn(doc, keys = [LEGACY_DEPENDENCY_KEY]) {
|
|
144
|
+
for (const key of keys) {
|
|
145
|
+
if (typeof doc?.dependencies?.[key] === "string") return key;
|
|
146
|
+
}
|
|
147
|
+
return "";
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function findProfileDir(startDir, { readJson = readJsonFile, maxDepth = 8, keys } = {}) {
|
|
151
|
+
const dependencyKeys = keys ?? dependencyKeysFor(startDir, { readJson });
|
|
152
|
+
let dir = resolve(startDir);
|
|
153
|
+
for (let i = 0; i < maxDepth; i++) {
|
|
154
|
+
try {
|
|
155
|
+
const doc = readJson(join(dir, "package.json"));
|
|
156
|
+
if (dependencyKeyIn(doc, dependencyKeys) !== "") return dir;
|
|
157
|
+
} catch {
|
|
158
|
+
// 该层没有 package.json 或不可读:继续向上
|
|
159
|
+
}
|
|
160
|
+
const parent = dirname(dir);
|
|
161
|
+
if (parent === dir) break;
|
|
162
|
+
dir = parent;
|
|
163
|
+
}
|
|
164
|
+
return "";
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function readJsonFile(file) {
|
|
168
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** 读 profile 里本插件的依赖声明;找不到返回空串(调用方据此转降级指引)。 */
|
|
172
|
+
export function profileSpecifierOf(profileDir, { readJson = readJsonFile, keys = [LEGACY_DEPENDENCY_KEY] } = {}) {
|
|
173
|
+
if (!profileDir) return "";
|
|
174
|
+
try {
|
|
175
|
+
const doc = readJson(join(profileDir, "package.json"));
|
|
176
|
+
const key = dependencyKeyIn(doc, keys);
|
|
177
|
+
return key === "" ? "" : doc.dependencies[key];
|
|
178
|
+
} catch {
|
|
179
|
+
return "";
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// C3: stable arbitration inode, kernel flock, and an unguessable generation fence.
|
|
184
|
+
// Metadata is never interpreted/removed by read->rename/rm outside this arbitration.
|
|
185
|
+
const arbiter = fileURLToPath(new URL("./update-lock-arbiter.js", import.meta.url));
|
|
186
|
+
function arbitrate(lockPath, request) {
|
|
187
|
+
mkdirSync(dirname(lockPath), { recursive: true });
|
|
188
|
+
const r = spawnSync("flock", ["--exclusive", "--wait", "5", `${lockPath}.arbitration`, process.execPath, arbiter], {
|
|
189
|
+
input: JSON.stringify({ ...request, lockPath }), encoding: "utf8", timeout: 7000,
|
|
190
|
+
});
|
|
191
|
+
if (r.error || r.status !== 0) return { ok: false, error: "lock-arbitration-unavailable" };
|
|
192
|
+
try { return JSON.parse(r.stdout); } catch { return { ok: false, error: "lock-arbitration-invalid" }; }
|
|
193
|
+
}
|
|
194
|
+
export function readLock(dir) {
|
|
195
|
+
try { return JSON.parse(readFileSync(join(dir, "holder.json"), "utf8")); } catch { return null; }
|
|
196
|
+
}
|
|
197
|
+
export function defaultIsPidAlive(pid) {
|
|
198
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
199
|
+
try { process.kill(pid, 0); return true; } catch(e) { return e.code === "EPERM"; }
|
|
200
|
+
}
|
|
201
|
+
export function lockHeld(dir) { return readLock(dir) !== null; }
|
|
202
|
+
export function acquireLock(dir, { opId, pid = process.pid, now = Date.now(), lease, stateFile } = {}) {
|
|
203
|
+
if (!validOperationId(opId)) return { ok: false, error: "invalid-operation-id" };
|
|
204
|
+
return arbitrate(dir, { operation: "acquire", opId, pid, now, lease, stateFile });
|
|
205
|
+
}
|
|
206
|
+
export function releaseLock(dir, lease, { stateFile } = {}) {
|
|
207
|
+
// An opId alone is not ownership. Old workers need the exact generation too.
|
|
208
|
+
if (!lease?.generation) return false;
|
|
209
|
+
return arbitrate(dir, { operation: "release", lease, stateFile }).ok;
|
|
210
|
+
}
|
|
211
|
+
export function handOverLock(dir, { lease, toPid } = {}) {
|
|
212
|
+
if (!lease?.generation) return false;
|
|
213
|
+
return arbitrate(dir, { operation: "handover", lease, toPid }).ok;
|
|
214
|
+
}
|
|
215
|
+
export function assertUpdateLease(lockPath, lease) {
|
|
216
|
+
const r = arbitrate(lockPath, { operation: "assert", lease });
|
|
217
|
+
if (!r.ok) throw new Error(r.error);
|
|
218
|
+
return r.holder;
|
|
219
|
+
}
|
|
220
|
+
export function writeOwnedTransaction(stateFile, tx, { lockPath, lease, currentFile, expectedPhases } = {}) {
|
|
221
|
+
const r = arbitrate(lockPath, { operation: "state-write", lease, stateFile, tx, currentFile, expectedPhases });
|
|
222
|
+
if (!r.ok) throw new Error(r.error);
|
|
223
|
+
return r.transaction;
|
|
224
|
+
}
|
|
225
|
+
export function switchOwnedDependency({ profileDir, nextSpecifier, key, lockPath, lease } = {}) {
|
|
226
|
+
return arbitrate(lockPath, { operation: "switch-profile", lease, profileDir, specifier: nextSpecifier, key });
|
|
227
|
+
}
|
|
228
|
+
export function claimOwnedRecovery(stateFile, { owner }, { lockPath, lease, currentFile } = {}) {
|
|
229
|
+
return arbitrate(lockPath, { operation: 'claim-recovery', lease, stateFile, owner, currentFile });
|
|
230
|
+
}
|
|
231
|
+
export function finishOwnedTerminal(stateFile, { phase, result, expectedPhases }, { lockPath, lease, currentFile } = {}) {
|
|
232
|
+
const r = arbitrate(lockPath, { operation: 'finish-terminal', lease, stateFile, phase, result, expectedPhases, currentFile });
|
|
233
|
+
if (!r.ok) throw new Error(r.error);
|
|
234
|
+
return r.transaction;
|
|
235
|
+
}
|
|
236
|
+
export function restoreOwnedProfile({ profileDir, snapshotDir, snapshot, key, preserveSpecifier = false, lockPath, lease } = {}) {
|
|
237
|
+
return arbitrate(lockPath, { operation: 'restore-profile', profileDir, snapshotDir, snapshot, key, preserveSpecifier, lease });
|
|
238
|
+
}
|
|
239
|
+
export function publishOwnedSnapshotLink({ profileDir, snapshotDir, key, lockPath, lease } = {}) {
|
|
240
|
+
return arbitrate(lockPath, { operation: 'publish-snapshot-link', profileDir, snapshotDir, key, lease });
|
|
241
|
+
}
|
|
242
|
+
export function validOperationId(opId) {
|
|
243
|
+
return typeof opId === "string" && /^[A-Za-z0-9][A-Za-z0-9_-]{0,95}$/.test(opId);
|
|
244
|
+
}
|
|
245
|
+
export function serverOperationPath(home, id) {
|
|
246
|
+
if (!/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(id)) throw new Error("invalid-filesystem-id");
|
|
247
|
+
return join(home, ".dsh", "mobile-remote", "transactions", `${id}.json`);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* 把当前已安装的插件内容快照成**自包含、可加载**的不可变副本(F4):
|
|
252
|
+
* - **解引用 symlink**:pnpm 布局下副本里的依赖是符号链接,若原样复制会指向树外的
|
|
253
|
+
* store;解引用后快照自己就是完整的依赖树;
|
|
254
|
+
* - **原子发布**:全部复制完成后才写 `.complete` 标记;中途失败留下的半成品不会被
|
|
255
|
+
* 当成可用快照(`snapshotReady` 会拒绝)。
|
|
256
|
+
*/
|
|
257
|
+
export function snapshotInstalled({ installedRoot, destDir, copyFile } = {}) {
|
|
258
|
+
if (!existsSync(installedRoot)) return { ok: false, error: "installed-root-missing" };
|
|
259
|
+
return freezePackage({ sourceRoot: installedRoot, destination: destDir, copyFile });
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** 默认复制实现:**解引用符号链接**,得到自包含树。 */
|
|
263
|
+
export function defaultCopyDir(src, dest) {
|
|
264
|
+
try {
|
|
265
|
+
mkdirSync(dest, { recursive: true });
|
|
266
|
+
copyRecursive(src, dest, new Set());
|
|
267
|
+
return { ok: true };
|
|
268
|
+
} catch (e) {
|
|
269
|
+
return { ok: false, error: String(e?.message ?? e) };
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function copyRecursive(src, dest, seen) {
|
|
274
|
+
// 防环:symlink 解引用后可能回到祖先
|
|
275
|
+
const key = `${src}->${dest}`;
|
|
276
|
+
if (seen.has(key)) return;
|
|
277
|
+
seen.add(key);
|
|
278
|
+
for (const entry of readdirSync(src, { withFileTypes: true })) {
|
|
279
|
+
const from = join(src, entry.name);
|
|
280
|
+
const to = join(dest, entry.name);
|
|
281
|
+
if (entry.isSymbolicLink()) {
|
|
282
|
+
// 解引用:把链接目标的内容复制过来(目标缺失则跳过,不制造悬空链接)
|
|
283
|
+
let real;
|
|
284
|
+
try {
|
|
285
|
+
real = realpathSync(from);
|
|
286
|
+
} catch {
|
|
287
|
+
throw new Error("required-link-broken");
|
|
288
|
+
}
|
|
289
|
+
const st = statSync(real);
|
|
290
|
+
if (st.isDirectory()) {
|
|
291
|
+
mkdirSync(to, { recursive: true });
|
|
292
|
+
copyRecursive(real, to, seen);
|
|
293
|
+
} else if (st.isFile()) {
|
|
294
|
+
writeFileSync(to, readFileSync(real));
|
|
295
|
+
}
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
if (entry.isDirectory()) {
|
|
299
|
+
mkdirSync(to, { recursive: true });
|
|
300
|
+
copyRecursive(from, to, seen);
|
|
301
|
+
} else if (entry.isFile()) {
|
|
302
|
+
writeFileSync(to, readFileSync(from));
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** 快照完成标记(原子发布的凭据)。 */
|
|
308
|
+
export const COMPLETE_MARKER = ".complete";
|
|
309
|
+
|
|
310
|
+
/** 目录是否存在且非空(用于判断产物是否可用)。 */
|
|
311
|
+
export function dirReady(dir) {
|
|
312
|
+
try {
|
|
313
|
+
return statSync(dir).isDirectory() && readdirSync(dir).length > 0;
|
|
314
|
+
} catch {
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** 快照是否**完整可用**:非空且带完成标记(半复制不会被当成回滚目标)。 */
|
|
320
|
+
export function snapshotReady(dir) {
|
|
321
|
+
return frozenPackageReady(dir);
|
|
322
|
+
}
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
// 插件侧的**更新判定**(CONTEXT「内容标识」「对齐提示」「降级指引」)。
|
|
2
|
+
//
|
|
3
|
+
// 这里只放纯逻辑,不做任何 I/O 决策之外的事:
|
|
4
|
+
// - 规范化发布清单;
|
|
5
|
+
// - 解析 tgz 的下载地址(清单里给 `url`,否则相对清单自身地址解析);
|
|
6
|
+
// - 比较「本地内容标识 vs 清单内容标识」决定要不要下载(版本号只做提示与排序);
|
|
7
|
+
// - 识别本机的**安装形态**,据此给出可执行动作或降级指引。
|
|
8
|
+
// 下载、校验、落位、pnpm、重启属于执行面,不在这里(见后续阶段的执行模块)。
|
|
9
|
+
//
|
|
10
|
+
// 三条判据互不代替:能力能不能用看能力声明;要不要下载看**内容标识**;要不要提示看版本号。
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
|
|
13
|
+
/** 本模块能处理的清单 schema。 */
|
|
14
|
+
export const SUPPORTED_MANIFEST_SCHEMA = 1;
|
|
15
|
+
|
|
16
|
+
/** 更新判定结果。 */
|
|
17
|
+
export const UPDATE_ACTION = {
|
|
18
|
+
/** 本地内容与清单一致:已是最新。 */
|
|
19
|
+
UP_TO_DATE: "up-to-date",
|
|
20
|
+
/** 清单内容与本地不同、且不早于本地:应更新。 */
|
|
21
|
+
UPDATE_AVAILABLE: "update-available",
|
|
22
|
+
/** 本地比清单新(例如本地装了更新的自建构建):不降级。 */
|
|
23
|
+
LOCAL_AHEAD: "local-ahead",
|
|
24
|
+
/** 清单不可用或字段缺失:无法判断,如实报出。 */
|
|
25
|
+
UNKNOWN: "unknown",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** 安装形态(决定升级动作,见 docs/06-install-run.md 的三种方式)。 */
|
|
29
|
+
export const INSTALL_SHAPE = {
|
|
30
|
+
/** 从 npm/registry 解析(`^3.2.0` 之类)。 */
|
|
31
|
+
REGISTRY: "registry",
|
|
32
|
+
/** `github:owner/repo#tag` 这类 git 依赖。 */
|
|
33
|
+
GITHUB_TAG: "github-tag",
|
|
34
|
+
/** `file:` 指向一个本地 clone(含 .git)。 */
|
|
35
|
+
FILE_CLONE: "file-clone",
|
|
36
|
+
/** `file:` 指向一个已解开的目录(非 git 仓库)。 */
|
|
37
|
+
FILE_DIR: "file-dir",
|
|
38
|
+
/** `file:` 指向一个 .tgz 包。 */
|
|
39
|
+
TGZ_FILE: "tgz-file",
|
|
40
|
+
/** 无法识别。 */
|
|
41
|
+
UNKNOWN: "unknown",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/** 更新执行方式:能自动完成,还是只能给出可复制的指引。 */
|
|
45
|
+
export const UPDATE_MODE = {
|
|
46
|
+
AUTO: "auto",
|
|
47
|
+
GUIDANCE: "guidance",
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 解析 `X.Y.Z`、可选 SemVer prerelease(如 `3.2.1-alpha.1`)与数字 `+N` build 号;不合法返回 null。
|
|
52
|
+
* `version` 是**只含 core** 的 `X.Y.Z` 字串:两端配对(App 的 `X.Y.Z+N` 与插件)只比它,
|
|
53
|
+
* prerelease 只影响插件自身的排序(见 compareVersion)。
|
|
54
|
+
*/
|
|
55
|
+
export function parseVersion(value) {
|
|
56
|
+
const m = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+(\d+))?$/
|
|
57
|
+
.exec(typeof value === "string" ? value.trim() : "");
|
|
58
|
+
if (!m) return null;
|
|
59
|
+
const prerelease = m[4] === undefined ? [] : m[4].split(".");
|
|
60
|
+
// SemVer §9:纯数字的 prerelease 标识符不得有前导零(`alpha.01` 非法,`alpha.1` 合法)。
|
|
61
|
+
for (const identifier of prerelease) {
|
|
62
|
+
if (/^\d+$/.test(identifier) && identifier.length > 1 && identifier.startsWith("0")) return null;
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
major: +m[1],
|
|
66
|
+
minor: +m[2],
|
|
67
|
+
patch: +m[3],
|
|
68
|
+
version: `${+m[1]}.${+m[2]}.${+m[3]}`,
|
|
69
|
+
prerelease,
|
|
70
|
+
build: m[5] === undefined ? null : +m[5],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** 比较两个 prerelease 标识符(SemVer §11.4:数字按数值比、数字 < 字母数字、字母数字按 ASCII)。 */
|
|
75
|
+
function comparePrereleaseIdentifier(a, b) {
|
|
76
|
+
const numericA = /^\d+$/.test(a);
|
|
77
|
+
const numericB = /^\d+$/.test(b);
|
|
78
|
+
if (numericA && numericB) {
|
|
79
|
+
// 无前导零的数字串:先比长度再比字典序即等价于按数值比较(超长数字也不丢精度)。
|
|
80
|
+
if (a.length !== b.length) return a.length < b.length ? -1 : 1;
|
|
81
|
+
if (a === b) return 0;
|
|
82
|
+
return a < b ? -1 : 1;
|
|
83
|
+
}
|
|
84
|
+
if (numericA) return -1;
|
|
85
|
+
if (numericB) return 1;
|
|
86
|
+
if (a === b) return 0;
|
|
87
|
+
return a < b ? -1 : 1;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** 比较 prerelease 段:无 prerelease 的版本优先级更高(3.2.1 > 3.2.1-alpha.1)。 */
|
|
91
|
+
function comparePrerelease(a, b) {
|
|
92
|
+
const left = a.prerelease;
|
|
93
|
+
const right = b.prerelease;
|
|
94
|
+
if (left.length === 0 && right.length === 0) return 0;
|
|
95
|
+
if (left.length === 0) return 1;
|
|
96
|
+
if (right.length === 0) return -1;
|
|
97
|
+
const length = Math.min(left.length, right.length);
|
|
98
|
+
for (let index = 0; index < length; index++) {
|
|
99
|
+
const comparison = comparePrereleaseIdentifier(left[index], right[index]);
|
|
100
|
+
if (comparison !== 0) return comparison;
|
|
101
|
+
}
|
|
102
|
+
if (left.length === right.length) return 0;
|
|
103
|
+
return left.length < right.length ? -1 : 1;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 比较两个版本号,返回 -1 / 0 / 1;任一不可解析时返回 null。
|
|
108
|
+
* 主序按 SemVer:core → prerelease(`3.2.1-alpha.1 < 3.2.1`、`alpha.2 > alpha.1`)。
|
|
109
|
+
* `+build` 是本仓既有扩展:只在 core 与 prerelease 都相同时参与比较
|
|
110
|
+
* (App 侧用它判断"有没有更新";两端配对看的是 core,见 compareSemanticVersion)。
|
|
111
|
+
*/
|
|
112
|
+
export function compareVersion(a, b) {
|
|
113
|
+
const pa = parseVersion(a);
|
|
114
|
+
const pb = parseVersion(b);
|
|
115
|
+
if (!pa || !pb) return null;
|
|
116
|
+
for (const k of ["major", "minor", "patch"]) {
|
|
117
|
+
if (pa[k] !== pb[k]) return pa[k] < pb[k] ? -1 : 1;
|
|
118
|
+
}
|
|
119
|
+
const prerelease = comparePrerelease(pa, pb);
|
|
120
|
+
if (prerelease !== 0) return prerelease;
|
|
121
|
+
if (pa.build !== null && pb.build !== null && pa.build !== pb.build) return pa.build < pb.build ? -1 : 1;
|
|
122
|
+
return 0;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 规范化发布清单。只接受 schema 1;`plugin` 段是插件自更新所必需的部分。
|
|
127
|
+
* 缺失字段不猜测:把可选的留空、必需的报错。
|
|
128
|
+
*/
|
|
129
|
+
export function normalizeReleaseManifest(raw) {
|
|
130
|
+
if (!raw || typeof raw !== "object") return { ok: false, error: "manifest-not-object" };
|
|
131
|
+
if (raw.schema !== SUPPORTED_MANIFEST_SCHEMA) {
|
|
132
|
+
return { ok: false, error: "manifest-schema-unsupported", schema: raw.schema };
|
|
133
|
+
}
|
|
134
|
+
const p = raw.plugin;
|
|
135
|
+
if (!p || typeof p !== "object") return { ok: false, error: "manifest-plugin-missing" };
|
|
136
|
+
if (typeof p.version !== "string" || !parseVersion(p.version)) return { ok: false, error: "manifest-plugin-version-invalid" };
|
|
137
|
+
if (typeof p.file !== "string" || p.file === "") return { ok: false, error: "manifest-plugin-file-missing" };
|
|
138
|
+
const sha256 = typeof p.sha256 === "string" ? p.sha256.toLowerCase() : "";
|
|
139
|
+
if (!/^[0-9a-f]{64}$/.test(sha256)) return { ok: false, error: "manifest-plugin-sha256-invalid" };
|
|
140
|
+
const contentSha256 = typeof p.contentSha256 === "string" ? p.contentSha256.toLowerCase() : "";
|
|
141
|
+
if (contentSha256 !== "" && !/^[0-9a-f]{64}$/.test(contentSha256)) {
|
|
142
|
+
return { ok: false, error: "manifest-plugin-content-sha256-invalid" };
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
ok: true,
|
|
146
|
+
schema: raw.schema,
|
|
147
|
+
plugin: {
|
|
148
|
+
version: p.version,
|
|
149
|
+
file: p.file,
|
|
150
|
+
url: typeof p.url === "string" && p.url !== "" ? p.url : "",
|
|
151
|
+
size: Number.isFinite(p.size) ? p.size : null,
|
|
152
|
+
sha256,
|
|
153
|
+
contentSha256,
|
|
154
|
+
},
|
|
155
|
+
app: raw.app && typeof raw.app === "object" ? raw.app : null,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* 解析插件包的下载地址:清单给了 `url` 就用它,否则相对**清单自身地址**解析。
|
|
161
|
+
* 两者都没有则返回空串(调用方按"来源不可用"处理)。
|
|
162
|
+
*/
|
|
163
|
+
export function resolvePluginUrl(manifestUrl, plugin) {
|
|
164
|
+
if (plugin?.url) return plugin.url;
|
|
165
|
+
if (!manifestUrl || !plugin?.file) return "";
|
|
166
|
+
try {
|
|
167
|
+
return new URL(plugin.file, manifestUrl).toString();
|
|
168
|
+
} catch {
|
|
169
|
+
return "";
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* 决定要不要下载新版本。
|
|
175
|
+
*
|
|
176
|
+
* 判据是**内容标识**:版本号相同而内容不同(真实存在的形态)必须被识别为"应更新"。
|
|
177
|
+
* 缺少内容标识时退化为版本比较,并把这一盲区写进 `reason`,不假装判断是完整的。
|
|
178
|
+
*/
|
|
179
|
+
export function decidePluginUpdate({ localVersion, localContentSha256, remote }) {
|
|
180
|
+
if (!remote || typeof remote !== "object" || !parseVersion(remote.version)) {
|
|
181
|
+
return { action: UPDATE_ACTION.UNKNOWN, reason: "remote-version-missing" };
|
|
182
|
+
}
|
|
183
|
+
const comparison = compareVersion(remote.version, localVersion ?? "");
|
|
184
|
+
const localDigest = (localContentSha256 ?? "").toLowerCase();
|
|
185
|
+
const remoteDigest = (remote.contentSha256 ?? "").toLowerCase();
|
|
186
|
+
|
|
187
|
+
if (localDigest && remoteDigest) {
|
|
188
|
+
if (localDigest === remoteDigest) {
|
|
189
|
+
return { action: UPDATE_ACTION.UP_TO_DATE, reason: "content-identical", comparison };
|
|
190
|
+
}
|
|
191
|
+
if (comparison !== null && comparison < 0) {
|
|
192
|
+
return { action: UPDATE_ACTION.LOCAL_AHEAD, reason: "content-differs-local-newer", comparison };
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
action: UPDATE_ACTION.UPDATE_AVAILABLE,
|
|
196
|
+
reason: comparison === 0 ? "content-differs-same-version" : "content-differs-remote-newer",
|
|
197
|
+
comparison,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (comparison === null) return { action: UPDATE_ACTION.UNKNOWN, reason: "local-version-unparseable" };
|
|
202
|
+
if (comparison > 0) return { action: UPDATE_ACTION.UPDATE_AVAILABLE, reason: "version-newer-no-content-digest", comparison };
|
|
203
|
+
if (comparison < 0) return { action: UPDATE_ACTION.LOCAL_AHEAD, reason: "version-older-no-content-digest", comparison };
|
|
204
|
+
// 版本相同又无内容标识:这是"看不见版本号相同内容不同"的盲区,如实标注
|
|
205
|
+
return { action: UPDATE_ACTION.UP_TO_DATE, reason: "version-equal-no-content-digest", comparison };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** 从依赖声明推断安装形态;纯字符串判断,不碰文件系统。 */
|
|
209
|
+
export function classifySpecifier(specifier) {
|
|
210
|
+
const s = typeof specifier === "string" ? specifier.trim() : "";
|
|
211
|
+
if (s === "") return INSTALL_SHAPE.UNKNOWN;
|
|
212
|
+
if (/^github:/i.test(s)) return INSTALL_SHAPE.GITHUB_TAG;
|
|
213
|
+
if (/^link:/i.test(s)) return INSTALL_SHAPE.UNKNOWN;
|
|
214
|
+
if (/^file:/i.test(s)) {
|
|
215
|
+
const target = s.slice("file:".length).toLowerCase();
|
|
216
|
+
if (target.endsWith(".tgz") || target.endsWith(".tar.gz")) return INSTALL_SHAPE.TGZ_FILE;
|
|
217
|
+
return INSTALL_SHAPE.FILE_DIR; // 目录形态;是否 git clone 需要文件系统确认
|
|
218
|
+
}
|
|
219
|
+
if (/^[\^~]?\d+\.\d+\.\d+/.test(s) || /^(>|>=|<|<=|=|\*|latest)/.test(s)) return INSTALL_SHAPE.REGISTRY;
|
|
220
|
+
return INSTALL_SHAPE.UNKNOWN;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* 探测安装形态。`specifier` 来自 profile 的 `package.json`;目录形态再用注入的谓词确认
|
|
225
|
+
* 是否 git 仓库。`deps` 可注入,便于在不碰真实文件系统的测试里驱动。
|
|
226
|
+
*/
|
|
227
|
+
export function detectInstallShape({ specifier, dirPath = "", isGitRepo = () => false } = {}) {
|
|
228
|
+
const base = classifySpecifier(specifier);
|
|
229
|
+
if (base === INSTALL_SHAPE.FILE_DIR && dirPath && isGitRepo(dirPath)) return INSTALL_SHAPE.FILE_CLONE;
|
|
230
|
+
return base;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* 该形态下如何更新。`auto` 表示本机可以自行完成(改依赖声明 + 安装 + 重启);
|
|
235
|
+
* `guidance` 表示只能给出**与实际安装方式对应的那一条**指引,不并列三条让用户猜。
|
|
236
|
+
*/
|
|
237
|
+
export function planForShape(shape, { specifier = "", dirPath = "" } = {}) {
|
|
238
|
+
const installCmd = "pnpm install";
|
|
239
|
+
switch (shape) {
|
|
240
|
+
case INSTALL_SHAPE.REGISTRY:
|
|
241
|
+
return {
|
|
242
|
+
mode: UPDATE_MODE.AUTO,
|
|
243
|
+
shape,
|
|
244
|
+
summary: "从 npm 源升级:改写 profile 依赖版本后重新安装",
|
|
245
|
+
steps: [`把 profile 的依赖版本改为新版(当前 ${specifier || "未知"})`, "重新安装依赖", "重启 DSH 让插件生效"],
|
|
246
|
+
commands: [installCmd],
|
|
247
|
+
};
|
|
248
|
+
case INSTALL_SHAPE.GITHUB_TAG:
|
|
249
|
+
return {
|
|
250
|
+
mode: UPDATE_MODE.AUTO,
|
|
251
|
+
shape,
|
|
252
|
+
summary: "git 依赖升级:把 tag 改到新版本后重新安装",
|
|
253
|
+
steps: [`把 profile 依赖的 tag 改为 v<新版>(当前 ${specifier || "未知"})`, "重新安装依赖", "重启 DSH 让插件生效"],
|
|
254
|
+
commands: [installCmd],
|
|
255
|
+
};
|
|
256
|
+
case INSTALL_SHAPE.FILE_CLONE:
|
|
257
|
+
return {
|
|
258
|
+
mode: UPDATE_MODE.AUTO,
|
|
259
|
+
shape,
|
|
260
|
+
summary: "本地 clone 升级:拉取新代码后重新安装",
|
|
261
|
+
steps: [`在 clone 目录拉取新版本${dirPath ? `(${dirPath})` : ""}`, "重新安装依赖", "重启 DSH 让插件生效"],
|
|
262
|
+
commands: [dirPath ? `git -C ${dirPath} pull` : "git pull", installCmd],
|
|
263
|
+
};
|
|
264
|
+
case INSTALL_SHAPE.FILE_DIR:
|
|
265
|
+
return {
|
|
266
|
+
mode: UPDATE_MODE.AUTO,
|
|
267
|
+
shape,
|
|
268
|
+
summary: "本地目录升级:用新版内容替换该目录后重新安装",
|
|
269
|
+
steps: [`用新版插件内容替换该目录${dirPath ? `(${dirPath})` : ""}`, "重新安装依赖", "重启 DSH 让插件生效"],
|
|
270
|
+
commands: [installCmd],
|
|
271
|
+
};
|
|
272
|
+
case INSTALL_SHAPE.TGZ_FILE:
|
|
273
|
+
return {
|
|
274
|
+
mode: UPDATE_MODE.AUTO,
|
|
275
|
+
shape,
|
|
276
|
+
summary: "tgz 安装升级:换上新版 tgz 后重新安装",
|
|
277
|
+
steps: [`下载新版 tgz 并替换${dirPath ? `(${dirPath})` : "原包"}`, "重新安装依赖", "重启 DSH 让插件生效"],
|
|
278
|
+
commands: [installCmd],
|
|
279
|
+
};
|
|
280
|
+
default:
|
|
281
|
+
return {
|
|
282
|
+
mode: UPDATE_MODE.GUIDANCE,
|
|
283
|
+
shape: INSTALL_SHAPE.UNKNOWN,
|
|
284
|
+
summary: "无法识别本机插件安装方式,按 docs/06-install-run.md 的三种方式之一处理",
|
|
285
|
+
steps: ["确认插件是通过 clone、git tag 还是 tgz 安装的", "按对应方式更新到新版", "重启 DSH 让插件生效"],
|
|
286
|
+
commands: [],
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* 对齐提示用的比较:只比 core `X.Y.Z`,`prerelease` 与 `+build` 都不参与。
|
|
293
|
+
* 契约:插件 `3.2.1-alpha.1` 与 App `3.2.1+24` 按 core `3.2.1` 视为对齐;
|
|
294
|
+
* 同一发布周期内每次构建/预发布都不应把提示刷成噪声。
|
|
295
|
+
*/
|
|
296
|
+
export function compareSemanticVersion(a, b) {
|
|
297
|
+
const pa = parseVersion(a);
|
|
298
|
+
const pb = parseVersion(b);
|
|
299
|
+
if (!pa || !pb) return null;
|
|
300
|
+
for (const k of ["major", "minor", "patch"]) {
|
|
301
|
+
if (pa[k] !== pb[k]) return pa[k] < pb[k] ? -1 : 1;
|
|
302
|
+
}
|
|
303
|
+
return 0;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** 计算某个文件的 sha256(发布侧与运行侧共用同一写法,避免大小写/编码差异)。 */
|
|
307
|
+
export function sha256Hex(buf) {
|
|
308
|
+
return createHash("sha256").update(buf).digest("hex");
|
|
309
|
+
}
|