dsh-config-manager 0.1.78 → 0.1.79
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/lib/adapters/credentials.d.ts +6 -2
- package/lib/adapters/credentials.js +19 -3
- package/lib/adapters/file-collection.js +2 -1
- package/lib/adapters/mcp.d.ts +48 -2
- package/lib/adapters/mcp.js +265 -3
- package/lib/adapters/messages.d.ts +3 -0
- package/lib/adapters/messages.js +7 -0
- package/lib/adapters/plugin-files.js +2 -1
- package/lib/adapters/plugins.js +105 -20
- package/lib/adapters/pnpm-workspace.d.ts +32 -0
- package/lib/adapters/pnpm-workspace.js +113 -2
- package/lib/adapters/prompts.d.ts +6 -2
- package/lib/adapters/prompts.js +17 -2
- package/lib/adapters/providers.d.ts +19 -2
- package/lib/adapters/providers.js +59 -3
- package/lib/adapters/settings.d.ts +6 -2
- package/lib/adapters/settings.js +13 -2
- package/lib/adapters/skills.d.ts +25 -1
- package/lib/adapters/skills.js +60 -0
- package/lib/adapters/ui.d.ts +6 -2
- package/lib/adapters/ui.js +13 -2
- package/lib/adapters/units.d.ts +18 -0
- package/lib/adapters/units.js +39 -0
- package/lib/adapters/workspaces.js +2 -1
- package/lib/client.d.ts +400 -112
- package/lib/client.js +2845 -1524
- package/lib/core/backup.js +20 -0
- package/lib/core/mcp-connector-store.d.ts +159 -0
- package/lib/core/mcp-connector-store.js +445 -0
- package/lib/core/messages-core.d.ts +2 -0
- package/lib/core/messages-core.js +4 -0
- package/lib/core/messages.d.ts +20 -0
- package/lib/core/messages.js +44 -0
- package/lib/core/phase3-host.js +18 -1
- package/lib/core/plugin-cli.d.ts +22 -2
- package/lib/core/plugin-cli.js +22 -3
- package/lib/core/recovery-orchestrator.d.ts +3 -1
- package/lib/core/recovery-orchestrator.js +75 -16
- package/lib/core/rollback-activity.d.ts +89 -0
- package/lib/core/rollback-activity.js +350 -0
- package/lib/core/rollback-ledger.d.ts +0 -9
- package/lib/core/rollback-ledger.js +37 -0
- package/lib/core/rollback-merge.d.ts +22 -2
- package/lib/core/rollback-merge.js +63 -13
- package/lib/core/rollback-point.d.ts +93 -0
- package/lib/core/rollback-point.js +420 -101
- package/lib/core/rollback-undo-journal.d.ts +123 -0
- package/lib/core/rollback-undo-journal.js +517 -0
- package/lib/core/self-update.js +2 -4
- package/lib/core/text-fidelity.d.ts +89 -0
- package/lib/core/text-fidelity.js +176 -0
- package/lib/core/types.d.ts +12 -3
- package/lib/core/update-check.js +2 -4
- package/lib/core/validator.d.ts +9 -0
- package/lib/core/validator.js +12 -0
- package/lib/foreign/bundle.js +42 -3
- package/lib/foreign/claude-code.js +358 -26
- package/lib/foreign/claude-sessions.d.ts +51 -8
- package/lib/foreign/claude-sessions.js +111 -13
- package/lib/foreign/read-claude-code.d.ts +33 -4
- package/lib/foreign/read-claude-code.js +446 -65
- package/lib/foreign/read-claude-plugins.d.ts +36 -0
- package/lib/foreign/read-claude-plugins.js +403 -0
- package/lib/foreign/registry.js +25 -2
- package/lib/foreign/truth-table.js +6 -4
- package/lib/foreign/types.d.ts +170 -2
- package/lib/index.d.ts +14 -5
- package/lib/index.js +60 -16
- package/lib/market/git-file-writer.js +7 -1
- package/lib/profiles/dsh-profile-launcher.d.ts +20 -0
- package/lib/profiles/dsh-profile-launcher.js +58 -1
- package/lib/profiles/dsh-profile-shared.d.ts +8 -2
- package/lib/profiles/dsh-profile-shared.js +8 -2
- package/lib/routes/foreign.js +8 -1
- package/lib/routes/index.d.ts +1 -1
- package/lib/routes/kit.d.ts +2 -2
- package/lib/routes/sync.js +173 -9
- package/lib/schema/manifest.d.ts +6 -1
- package/lib/schema/manifest.js +55 -0
- package/lib/schema/types.d.ts +47 -2
- package/lib/sync/autosync-config.d.ts +73 -0
- package/lib/sync/autosync-config.js +89 -0
- package/lib/sync/autosync-scheduler.d.ts +80 -6
- package/lib/sync/autosync-scheduler.js +263 -40
- package/lib/sync/gist/gist-transport.d.ts +15 -1
- package/lib/sync/gist/gist-transport.js +66 -5
- package/lib/sync/gist/messages.d.ts +3 -0
- package/lib/sync/gist/messages.js +7 -0
- package/lib/sync/git/git-transport.d.ts +42 -3
- package/lib/sync/git/git-transport.js +146 -3
- package/lib/sync/merge.d.ts +47 -14
- package/lib/sync/merge.js +242 -222
- package/lib/sync/messages.d.ts +8 -0
- package/lib/sync/messages.js +19 -0
- package/lib/sync/risk.d.ts +19 -2
- package/lib/sync/risk.js +10 -3
- package/lib/sync/s3/messages.d.ts +6 -0
- package/lib/sync/s3/messages.js +13 -0
- package/lib/sync/s3/s3-transport.d.ts +9 -1
- package/lib/sync/s3/s3-transport.js +78 -6
- package/lib/sync/sync-engine.d.ts +193 -4
- package/lib/sync/sync-engine.js +450 -37
- package/lib/sync/sync-history.d.ts +11 -0
- package/lib/sync/sync-history.js +20 -0
- package/lib/sync/sync-state.d.ts +43 -0
- package/lib/sync/sync-state.js +44 -0
- package/lib/sync/sync-watcher.d.ts +76 -0
- package/lib/sync/sync-watcher.js +202 -0
- package/lib/sync/text-merge.d.ts +85 -0
- package/lib/sync/text-merge.js +513 -0
- package/lib/sync/transport.d.ts +48 -0
- package/lib/sync/transport.js +12 -0
- package/lib/sync/watch-scope.d.ts +38 -0
- package/lib/sync/watch-scope.js +95 -0
- package/lib/sync/webdav/webdav-transport.d.ts +16 -2
- package/lib/sync/webdav/webdav-transport.js +128 -11
- package/lib/ui/foreign-view.js +10 -0
- package/lib/ui/i18n.d.ts +8 -0
- package/lib/ui/i18n.js +20 -0
- package/lib/ui/rollback-view.d.ts +12 -0
- package/lib/ui/rollback-view.js +2 -0
- package/lib/ui/sync-settings-view.d.ts +47 -0
- package/lib/ui/sync-settings-view.js +24 -0
- package/lib/ui/types.d.ts +21 -0
- package/lib/utils/env-lock.d.ts +1 -0
- package/lib/utils/env-lock.js +7 -4
- package/package.json +6 -6
- package/src/adapters/credentials.ts +22 -4
- package/src/adapters/file-collection.ts +2 -1
- package/src/adapters/mcp.test.ts +235 -1
- package/src/adapters/mcp.ts +276 -5
- package/src/adapters/messages.ts +7 -0
- package/src/adapters/plugin-files.ts +2 -1
- package/src/adapters/plugins.test.ts +176 -2
- package/src/adapters/plugins.ts +107 -21
- package/src/adapters/pnpm-workspace.test.ts +89 -1
- package/src/adapters/pnpm-workspace.ts +127 -2
- package/src/adapters/prompts.ts +20 -3
- package/src/adapters/providers.ts +62 -4
- package/src/adapters/settings.ts +16 -3
- package/src/adapters/skills.test.ts +141 -0
- package/src/adapters/skills.ts +58 -2
- package/src/adapters/ui.ts +16 -3
- package/src/adapters/units-entries.test.ts +190 -0
- package/src/adapters/units.ts +50 -0
- package/src/adapters/workspaces.ts +2 -1
- package/src/client/about/PluginUpdateDialog.tsx +4 -2
- package/src/client/about/ReleaseNotesDialog.tsx +4 -2
- package/src/client/chat/RollbackReviewDialog.tsx +3 -2
- package/src/client/common/Skeleton.tsx +32 -0
- package/src/client/common/routes.ts +9 -0
- package/src/client/config-manager.module.css +321 -11
- package/src/client/home/HomePanel.tsx +14 -6
- package/src/client/import/ForeignImportView.tsx +3 -1
- package/src/client/import/import-wizard-steps.tsx +2 -1
- package/src/client/library/ArtifactRow.tsx +12 -2
- package/src/client/library/LibraryActions.tsx +6 -3
- package/src/client/library/LibraryPanel.tsx +21 -3
- package/src/client/library/RestorePlanView.tsx +4 -2
- package/src/client/locales.ts +3 -0
- package/src/client/recovery/RecoveryPanel.tsx +54 -7
- package/src/client/recovery/RollbackCard.tsx +7 -2
- package/src/client/recovery/RollbackDiffPane.tsx +3 -2
- package/src/client/recovery/recovery-locales.ts +20 -2
- package/src/client/recovery/recovery-view.test.ts +39 -1
- package/src/client/recovery/recovery-view.ts +23 -1
- package/src/client/run-store.test.ts +1 -1
- package/src/client/run-store.ts +4 -0
- package/src/client/sync/AutosyncCard.tsx +49 -6
- package/src/client/sync/MergeModal.tsx +219 -0
- package/src/client/sync/MergePreviewView.tsx +306 -0
- package/src/client/sync/SyncActivityBar.tsx +31 -0
- package/src/client/sync/SyncConfirmView.tsx +2 -1
- package/src/client/sync/SyncPanel.tsx +130 -87
- package/src/client/sync/SyncProbeRow.tsx +162 -0
- package/src/client/sync/SyncSettingsView.test.ts +27 -2
- package/src/client/sync/WorkDirCard.tsx +44 -6
- package/src/client/sync/history-model.ts +4 -1
- package/src/client/sync/merge-view.test.ts +234 -0
- package/src/client/sync/merge-view.ts +371 -0
- package/src/client/sync/probe-view.test.ts +90 -0
- package/src/client/sync/probe-view.ts +63 -0
- package/src/client/sync/remote-device-view.test.ts +35 -0
- package/src/client/sync/remote-device-view.ts +25 -0
- package/src/client/sync/sync-api.ts +212 -2
- package/src/client/sync/sync-entry.test.ts +78 -0
- package/src/client/sync/sync-entry.ts +53 -0
- package/src/client/sync/sync-locales.ts +234 -2
- package/src/client/sync/sync-view.test.ts +18 -1
- package/src/client/sync/sync-view.ts +20 -0
- package/src/core/backup.test.ts +41 -0
- package/src/core/backup.ts +20 -0
- package/src/core/mcp-connector-store.test.ts +265 -0
- package/src/core/mcp-connector-store.ts +516 -0
- package/src/core/messages-core.ts +4 -0
- package/src/core/messages.ts +46 -0
- package/src/core/phase3-host.ts +18 -1
- package/src/core/phase3-production-integration.test.ts +4 -0
- package/src/core/plugin-cli.test.ts +26 -0
- package/src/core/plugin-cli.ts +30 -4
- package/src/core/recovery-orchestrator.test.ts +104 -5
- package/src/core/recovery-orchestrator.ts +74 -17
- package/src/core/rollback-activity.test.ts +311 -0
- package/src/core/rollback-activity.ts +367 -0
- package/src/core/rollback-cross-session.test.ts +694 -0
- package/src/core/rollback-interrupted.test.ts +137 -0
- package/src/core/rollback-ledger.ts +32 -0
- package/src/core/rollback-merge.test.ts +130 -0
- package/src/core/rollback-merge.ts +78 -14
- package/src/core/rollback-point.ts +468 -91
- package/src/core/rollback-undo-journal.test.ts +508 -0
- package/src/core/rollback-undo-journal.ts +571 -0
- package/src/core/self-update.ts +2 -5
- package/src/core/text-fidelity.test.ts +189 -0
- package/src/core/text-fidelity.ts +197 -0
- package/src/core/types.ts +11 -3
- package/src/core/update-check.ts +2 -5
- package/src/core/validator.ts +14 -0
- package/src/foreign/bundle-origin.test.ts +265 -0
- package/src/foreign/bundle.ts +45 -5
- package/src/foreign/claude-code.test.ts +268 -0
- package/src/foreign/claude-code.ts +406 -25
- package/src/foreign/claude-plugins.test.ts +234 -0
- package/src/foreign/claude-sessions.test.ts +141 -0
- package/src/foreign/claude-sessions.ts +179 -23
- package/src/foreign/file-budget.test.ts +4 -1
- package/src/foreign/read-claude-code.test.ts +320 -0
- package/src/foreign/read-claude-code.ts +490 -71
- package/src/foreign/read-claude-plugins.ts +465 -0
- package/src/foreign/registry.ts +25 -2
- package/src/foreign/session-ir.test.ts +65 -0
- package/src/foreign/truth-table.test.ts +81 -0
- package/src/foreign/truth-table.ts +6 -3
- package/src/foreign/types.ts +162 -2
- package/src/index.facade.test.ts +26 -4
- package/src/index.ts +76 -19
- package/src/market/git-file-writer.test.ts +4 -0
- package/src/market/git-file-writer.ts +7 -1
- package/src/profiles/dsh-profile-launcher.test.ts +68 -3
- package/src/profiles/dsh-profile-launcher.ts +56 -1
- package/src/profiles/dsh-profile-shared.ts +8 -2
- package/src/routes/foreign.ts +8 -1
- package/src/routes/index.ts +1 -1
- package/src/routes/kit.ts +2 -2
- package/src/routes/sync.ts +176 -9
- package/src/schema/manifest-schema.test.ts +87 -2
- package/src/schema/manifest.ts +57 -1
- package/src/schema/types.ts +45 -2
- package/src/sync/autosync-config.test.ts +73 -1
- package/src/sync/autosync-config.ts +124 -0
- package/src/sync/autosync-risk-tier.test.ts +38 -2
- package/src/sync/autosync-scheduler.test.ts +264 -12
- package/src/sync/autosync-scheduler.ts +296 -44
- package/src/sync/gist/gist-conditional-write.test.ts +223 -0
- package/src/sync/gist/gist-transport.ts +73 -7
- package/src/sync/gist/messages.ts +7 -0
- package/src/sync/git/git-probe.test.ts +80 -0
- package/src/sync/git/git-transport.test.ts +185 -0
- package/src/sync/git/git-transport.ts +178 -5
- package/src/sync/merge.test.ts +188 -55
- package/src/sync/merge.ts +282 -224
- package/src/sync/messages.ts +19 -0
- package/src/sync/risk.ts +29 -5
- package/src/sync/s3/messages.ts +13 -0
- package/src/sync/s3/s3-conditional-write.test.ts +330 -0
- package/src/sync/s3/s3-transport.ts +87 -9
- package/src/sync/sync-device-identity.test.ts +267 -0
- package/src/sync/sync-engine.test.ts +379 -0
- package/src/sync/sync-engine.ts +535 -36
- package/src/sync/sync-history.test.ts +16 -1
- package/src/sync/sync-history.ts +26 -0
- package/src/sync/sync-state.test.ts +64 -0
- package/src/sync/sync-state.ts +79 -0
- package/src/sync/sync-watcher.test.ts +197 -0
- package/src/sync/sync-watcher.ts +246 -0
- package/src/sync/text-merge.test.ts +358 -0
- package/src/sync/text-merge.ts +554 -0
- package/src/sync/transport.ts +57 -0
- package/src/sync/watch-scope.test.ts +122 -0
- package/src/sync/watch-scope.ts +120 -0
- package/src/sync/webdav/webdav-conditional-write.test.ts +360 -0
- package/src/sync/webdav/webdav-transport.ts +137 -14
- package/src/ui/foreign-view.test.ts +34 -0
- package/src/ui/foreign-view.ts +10 -0
- package/src/ui/i18n.ts +20 -0
- package/src/ui/rollback-view.ts +14 -0
- package/src/ui/sync-settings-view.test.ts +28 -1
- package/src/ui/sync-settings-view.ts +43 -0
- package/src/ui/types.ts +22 -0
- package/src/utils/env-lock.ts +8 -5
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 文本保真原语的单元测试(2026-10-18 保真缺陷):**保真只能靠字节证明**。
|
|
3
|
+
*
|
|
4
|
+
* 每条断言都用**真实字节**收口 —— 尤其是判据 1(decode ∘ encode 逐字节恒等),
|
|
5
|
+
* 因为「函数返回了 ok:true」对「文件没被写坏」不构成任何证据。
|
|
6
|
+
*/
|
|
7
|
+
import test from 'node:test'
|
|
8
|
+
import assert from 'node:assert/strict'
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
decodeTextBytes,
|
|
12
|
+
encodeTextBytes,
|
|
13
|
+
isUtf8RoundTrippable,
|
|
14
|
+
probeTextForm,
|
|
15
|
+
type TextShape,
|
|
16
|
+
} from './text-fidelity.ts'
|
|
17
|
+
|
|
18
|
+
const enc = (text: string): number[] => [...new TextEncoder().encode(text)]
|
|
19
|
+
/** 组装字节:可选 BOM 前缀 + 文本的 UTF-8 字节 */
|
|
20
|
+
const bytesOf = (text: string, bom = false): number[] => [...(bom ? [0xef, 0xbb, 0xbf] : []), ...enc(text)]
|
|
21
|
+
|
|
22
|
+
/** 断言「可解码」并取出文本与形态(失败即测试失败,不做 any 兜底) */
|
|
23
|
+
function decodeOk(bytes: number[] | Uint8Array): { text: string; shape: TextShape } {
|
|
24
|
+
const result = decodeTextBytes(bytes instanceof Uint8Array ? bytes : Uint8Array.from(bytes))
|
|
25
|
+
if (!result.ok) throw new Error('期望解码成功,实际被拒:' + result.reason)
|
|
26
|
+
return { text: result.text, shape: result.shape }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** 断言「被拒绝」并回传 reason */
|
|
30
|
+
function decodeReason(bytes: number[] | Uint8Array): 'binary' | 'non-utf8' {
|
|
31
|
+
const result = decodeTextBytes(bytes instanceof Uint8Array ? bytes : Uint8Array.from(bytes))
|
|
32
|
+
if (result.ok) throw new Error('期望被拒绝,实际解码成功:' + JSON.stringify(result.text))
|
|
33
|
+
return result.reason
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** 判据 1:解码再编码必须逐字节回到原字节 */
|
|
37
|
+
function assertByteExactRoundTrip(bytes: number[], label: string): void {
|
|
38
|
+
const decoded = decodeOk(bytes)
|
|
39
|
+
const again = encodeTextBytes(decoded.text, decoded.shape)
|
|
40
|
+
assert.deepEqual([...again], bytes, label + ':decode ∘ encode 必须逐字节恒等')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
test('F-01 空文件:0 字节可解码、形态取缺省、往返仍是 0 字节', () => {
|
|
44
|
+
const { text, shape } = decodeOk([])
|
|
45
|
+
assert.equal(text, '')
|
|
46
|
+
assert.deepEqual(shape, { eol: '\n', bom: false })
|
|
47
|
+
assert.deepEqual([...encodeTextBytes(text, shape)], [])
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('F-02 单行无尾换行:可往返,主导行尾取缺省 LF', () => {
|
|
51
|
+
const bytes = bytesOf('const a = 1')
|
|
52
|
+
const { text, shape } = decodeOk(bytes)
|
|
53
|
+
assert.equal(text, 'const a = 1')
|
|
54
|
+
assert.equal(shape.eol, '\n')
|
|
55
|
+
assertByteExactRoundTrip(bytes, 'F-02')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('F-03 LF 多行 + 尾换行:主导行尾 LF', () => {
|
|
59
|
+
const bytes = bytesOf('a\nb\nc\n')
|
|
60
|
+
const { text, shape } = decodeOk(bytes)
|
|
61
|
+
assert.equal(text, 'a\nb\nc\n')
|
|
62
|
+
assert.equal(shape.eol, '\n')
|
|
63
|
+
assertByteExactRoundTrip(bytes, 'F-03')
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('F-04 CRLF:text 原样保留 CRLF(不改写行尾)、主导行尾 CRLF、往返逐字节相同', () => {
|
|
67
|
+
const bytes = bytesOf('a\r\nb\r\nc\r\n')
|
|
68
|
+
const { text, shape } = decodeOk(bytes)
|
|
69
|
+
assert.equal(text, 'a\r\nb\r\nc\r\n', 'decode 绝不把 CRLF 折成 LF')
|
|
70
|
+
assert.equal(shape.eol, '\r\n')
|
|
71
|
+
assertByteExactRoundTrip(bytes, 'F-04')
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('F-05 混合行尾:主导取严格多者、text 逐字节保留混合形态(往返仍恒等)', () => {
|
|
75
|
+
// 2 个 CRLF + 1 个 LF ⇒ 主导 CRLF
|
|
76
|
+
const bytes = bytesOf('a\r\nb\r\nc\nd\r\n')
|
|
77
|
+
const { text, shape } = decodeOk(bytes)
|
|
78
|
+
assert.equal(shape.eol, '\r\n')
|
|
79
|
+
assert.equal(text, 'a\r\nb\r\nc\nd\r\n')
|
|
80
|
+
assert.equal(probeTextForm(text).eols.length, 2, '如实报告「两种行尾都出现过」')
|
|
81
|
+
assertByteExactRoundTrip(bytes, 'F-05')
|
|
82
|
+
// 反例方向:2 个 LF + 1 个 CRLF ⇒ 主导 LF
|
|
83
|
+
const reversed = bytesOf('a\nb\nc\r\nd\n')
|
|
84
|
+
assert.equal(decodeOk(reversed).shape.eol, '\n')
|
|
85
|
+
assertByteExactRoundTrip(reversed, 'F-05b')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test('F-06 混合行尾平局(1 CRLF + 1 LF)⇒ 取 LF(不猜、且往返恒等)', () => {
|
|
89
|
+
const bytes = bytesOf('a\r\nb\nc\n')
|
|
90
|
+
const { shape } = decodeOk(bytes)
|
|
91
|
+
assert.equal(shape.eol, '\n', '严格多者胜:相等时回落到 LF')
|
|
92
|
+
assertByteExactRoundTrip(bytes, 'F-06')
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test('F-07 带 BOM:decode 剥掉 3 字节 BOM 并记账,encode 原样补回(逐字节恒等)', () => {
|
|
96
|
+
const bytes = bytesOf('a\r\nb\r\n', true)
|
|
97
|
+
const { text, shape } = decodeOk(bytes)
|
|
98
|
+
assert.equal(shape.bom, true)
|
|
99
|
+
assert.equal(text, 'a\r\nb\r\n', '正文里不再残留 U+FEFF')
|
|
100
|
+
assert.equal(text.startsWith('\uFEFF'), false)
|
|
101
|
+
assert.deepEqual([...encodeTextBytes(text, shape)].slice(0, 3), [0xef, 0xbb, 0xbf])
|
|
102
|
+
assertByteExactRoundTrip(bytes, 'F-07')
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
test('F-08 无 BOM:shape.bom 为 false,encode 不加前缀', () => {
|
|
106
|
+
const bytes = bytesOf('a\nb\n')
|
|
107
|
+
const { shape } = decodeOk(bytes)
|
|
108
|
+
assert.equal(shape.bom, false)
|
|
109
|
+
assert.deepEqual([...encodeTextBytes('x', { eol: '\n', bom: false })], [0x78])
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
test('F-09 Latin-1(0xE9)⇒ non-utf8(fail-closed,绝不按 U+FFFD 读出再写回)', () => {
|
|
113
|
+
// "café" 的 Latin-1 字节:0xE9 不是合法 UTF-8 起始字节
|
|
114
|
+
assert.equal(decodeReason([0x63, 0x61, 0x66, 0xe9]), 'non-utf8')
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
test('F-10 GBK 中文(D6 D0 CE C4)⇒ non-utf8', () => {
|
|
118
|
+
assert.equal(decodeReason([0xd6, 0xd0, 0xce, 0xc4]), 'non-utf8')
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
test('F-11 UTF-16LE 带 BOM(FF FE …)⇒ non-utf8', () => {
|
|
122
|
+
assert.equal(decodeReason([0xff, 0xfe, 0x2d, 0x4e, 0x87, 0x65]), 'non-utf8')
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
test('F-12 已知口径:无 BOM 且恰好也是合法 UTF-8 的 UTF-16LE 不做猜测(往返仍恒等 ⇒ 不会写坏字节)', () => {
|
|
126
|
+
// '你好' 的 UTF-16LE = 60 4F 7D 59 —— 4 个合法 ASCII 字节,字节层面无法与 UTF-8 区分。
|
|
127
|
+
const bytes = [0x60, 0x4f, 0x7d, 0x59]
|
|
128
|
+
const { text, shape } = decodeOk(bytes)
|
|
129
|
+
assert.deepEqual([...text].map((char) => char.charCodeAt(0)), bytes, '不猜编码:按 UTF-8 如实读出')
|
|
130
|
+
assert.deepEqual([...encodeTextBytes(text, shape)], bytes, '关键:往返逐字节相同,绝不写坏')
|
|
131
|
+
assertByteExactRoundTrip(bytes, 'F-12')
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
test('F-13 含 NUL ⇒ binary(口径 = 前 8 KiB,与既有实现一致)', () => {
|
|
135
|
+
assert.equal(decodeReason([0x61, 0x00, 0x62]), 'binary')
|
|
136
|
+
assert.equal(decodeReason([0x00]), 'binary')
|
|
137
|
+
// 边界:第 8193 个字节才是 NUL —— 与既有「前 8 KiB」口径一致,不额外扩大判定
|
|
138
|
+
const late = [...enc('a'.repeat(8192)), 0x00]
|
|
139
|
+
assert.equal(decodeOk(late).text.length, 8193)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
test('F-14 孤立 CR(老 Mac 行尾)⇒ non-utf8(TextShape 表示不了 ⇒ 拒绝,绝不悄悄改成 LF)', () => {
|
|
143
|
+
assert.equal(decodeReason(bytesOf('a\rb\r')), 'non-utf8')
|
|
144
|
+
// 但 CRLF 里的 CR 不算孤立
|
|
145
|
+
assert.equal(decodeOk(bytesOf('a\r\nb')).shape.eol, '\r\n')
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
test('F-15 表驱动:判据 1(逐字节恒等)覆盖 LF / CRLF / 混合 / BOM / 中文 / emoji / 空行', () => {
|
|
149
|
+
const fixtures: Array<{ label: string; bytes: number[] }> = [
|
|
150
|
+
{ label: '空文件', bytes: [] },
|
|
151
|
+
{ label: '只有换行', bytes: bytesOf('\n') },
|
|
152
|
+
{ label: '空行夹杂', bytes: bytesOf('a\n\nb\n') },
|
|
153
|
+
{ label: '中文 LF', bytes: bytesOf('你好\n世界\n') },
|
|
154
|
+
{ label: 'emoji(合法代理对)', bytes: bytesOf('ok 😀 done\n') },
|
|
155
|
+
{ label: 'CRLF 中文', bytes: bytesOf('你好\r\n世界\r\n') },
|
|
156
|
+
{ label: 'BOM + CRLF', bytes: bytesOf('a\r\nb\r\n', true) },
|
|
157
|
+
{ label: 'BOM + 中文 LF', bytes: bytesOf('你好\n', true) },
|
|
158
|
+
{ label: '混合行尾', bytes: bytesOf('a\r\nb\nc\r\n') },
|
|
159
|
+
{ label: '单行无尾换行', bytes: bytesOf('x = 1') },
|
|
160
|
+
{ label: '尾随空行', bytes: bytesOf('a\n\n') },
|
|
161
|
+
]
|
|
162
|
+
for (const fixture of fixtures) assertByteExactRoundTrip(fixture.bytes, 'F-15 ' + fixture.label)
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
test('F-16 代理项判据:合法代理对不误判,孤立代理项必须判 false', () => {
|
|
166
|
+
assert.equal(isUtf8RoundTrippable('😀'), true)
|
|
167
|
+
assert.equal(isUtf8RoundTrippable('\uD83D\uDE00'), true)
|
|
168
|
+
assert.equal(isUtf8RoundTrippable('\uD83D'), false, '孤立高代理项')
|
|
169
|
+
assert.equal(isUtf8RoundTrippable('\uDE00'), false, '孤立低代理项')
|
|
170
|
+
assert.equal(isUtf8RoundTrippable('a\uD83Db'), false, '高代理项后面不是低代理项')
|
|
171
|
+
// 这就是必须拒绝的原因:TextEncoder 会把孤立代理项写成 U+FFFD(内容损毁)
|
|
172
|
+
assert.deepEqual([...new TextEncoder().encode('\uD83D')], [0xef, 0xbf, 0xbd])
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
test('F-17 encodeTextBytes:bom=false 不动字节;bom=true 恰好加 3 字节前缀且不改行尾', () => {
|
|
176
|
+
const text = 'a\r\nb'
|
|
177
|
+
assert.deepEqual([...encodeTextBytes(text, { eol: '\n', bom: false })], enc(text), 'eol 不参与字节生成')
|
|
178
|
+
assert.deepEqual([...encodeTextBytes(text, { eol: '\r\n', bom: false })], enc(text))
|
|
179
|
+
assert.deepEqual([...encodeTextBytes(text, { eol: '\n', bom: true })], [0xef, 0xbb, 0xbf, ...enc(text)])
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
test('F-18 probeTextForm:纯描述(主导行尾 / BOM 前缀 / 出现过的行尾 / 孤立 CR 计数)', () => {
|
|
183
|
+
assert.deepEqual(probeTextForm('a\nb\n'), { eol: '\n', bom: false, eols: ['\n'], loneCr: 0 })
|
|
184
|
+
assert.deepEqual(probeTextForm('a\r\nb\r\n'), { eol: '\r\n', bom: false, eols: ['\r\n'], loneCr: 0 })
|
|
185
|
+
assert.deepEqual(probeTextForm('a\r\nb\n'), { eol: '\n', bom: false, eols: ['\r\n', '\n'], loneCr: 0 })
|
|
186
|
+
assert.deepEqual(probeTextForm('a\rb'), { eol: '\n', bom: false, eols: [], loneCr: 1 })
|
|
187
|
+
assert.deepEqual(probeTextForm('\uFEFFa\n'), { eol: '\n', bom: true, eols: ['\n'], loneCr: 0 })
|
|
188
|
+
assert.deepEqual(probeTextForm(''), { eol: '\n', bom: false, eols: [], loneCr: 0 })
|
|
189
|
+
})
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 文本保真(text fidelity)—— 字节 ↔ 文本的**无损往返**与「形态」(行尾 / BOM)判定。
|
|
3
|
+
*
|
|
4
|
+
* 为什么需要它(2026-10-18 保真缺陷):
|
|
5
|
+
* 回滚 / 跨对话三方合并是**文本**级算法,但工作区文件是**字节**。用 'Buffer.toString("utf8")' 去解一个
|
|
6
|
+
* GBK / Latin-1 文件**不会抛错**(非法字节被替换成 U+FFFD),合并结果再按 UTF-8 写回 ⇒ 原文件被
|
|
7
|
+
* **mojibake 覆盖、内容不可逆损毁**。换行符同理:把 CRLF 折成 LF 再 join('\n'),整份文件的行尾被
|
|
8
|
+
* 静默改写(Windows 上 = 全文件 diff)。
|
|
9
|
+
*
|
|
10
|
+
* 两条硬规矩:
|
|
11
|
+
* ① **无损或拒绝**:解出来的文本必须能逐字节编码回原字节(decodeTextBytes 自己证明这一点,见判据 1);
|
|
12
|
+
* 做不到就 ok:false,调用方必须**零写入** —— 绝不「差不多就行」。
|
|
13
|
+
* ② **形态不猜**:行尾只有 '\n' | '\r\n' 两种可无损表示的形式;孤立 '\r'(老 Mac 风格)无法用它们表示
|
|
14
|
+
* ⇒ 同样 ok:false,而不是悄悄把它当 LF 写掉。
|
|
15
|
+
*
|
|
16
|
+
* ── 归一化口径(**唯一的契约**,判据 1 就建立在这三条上)──
|
|
17
|
+
* · decodeTextBytes **只做一件事**:剥掉开头的 UTF-8 BOM(EF BB BF)。**不改写任何行尾** ——
|
|
18
|
+
* 返回的 text 里 CRLF / LF 原样保留,因此**混合行尾**的文件也能逐字节往返。
|
|
19
|
+
* · shape.eol 描述这份文本的**主导行尾**(完整的 CRLF 数**严格多于**纯 LF 数时为 '\r\n';
|
|
20
|
+
* 相等 / 没有整行行尾 / 空文本一律 '\n')。它供「需要新建行」的一方(三方合并)决定输出行尾,
|
|
21
|
+
* **不影响** encodeTextBytes 的字节。
|
|
22
|
+
* · encodeTextBytes(text, shape) **只做一件事**:shape.bom 为真时补回 3 字节 BOM。**不改写任何行尾**。
|
|
23
|
+
* 于是恒有 encodeTextBytes(d.text, d.shape) 逐字节等于原始 bytes(d = decodeTextBytes(bytes))。
|
|
24
|
+
* · 行尾的**归一化只发生在三方合并**(rollback-merge.ts):输出按 shape.eol 统一,并用 eolMixed /
|
|
25
|
+
* eolRewritten 如实标注,绝不假装「原样保留」。
|
|
26
|
+
*
|
|
27
|
+
* ── 已知口径(**不猜、不谎报**)──
|
|
28
|
+
* · 前 8 KiB 出现 NUL 字节 ⇒ 'binary'(与 utils/line-diff.ts、rollback-point.ts 同一口径)。
|
|
29
|
+
* · 非法 UTF-8(超长编码、孤立代理项的 CESU/WTF-8 编码、UTF-16 的 FF FE 前缀等)⇒ 'non-utf8'。
|
|
30
|
+
* · 「恰好也是合法 UTF-8 的无 BOM UTF-16LE」在**字节层面无法与 UTF-8 区分**(例:UTF-16LE 的
|
|
31
|
+
* 你好 = 60 4F 7D 59,是 4 个合法 ASCII 字节)。本模块**不猜编码**:它会被当作合法 UTF-8 读出,
|
|
32
|
+
* 但往返仍逐字节相同 ⇒ **不会写坏字节**(要「猜编码」需要额外启发式,那才是真正的越权)。
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/** 行尾形态:只有 LF 与 CRLF 两种能被无损表示的换行符(孤立 CR 一律拒绝,见模块头 ②)。 */
|
|
36
|
+
export type TextEol = '\n' | '\r\n'
|
|
37
|
+
|
|
38
|
+
/** 一份文本的「形态」:主导行尾 + 是否带 UTF-8 BOM。 */
|
|
39
|
+
export interface TextShape {
|
|
40
|
+
/** 主导行尾(严格多者胜;相等 / 无行尾 / 空文本 ⇒ '\n') */
|
|
41
|
+
eol: TextEol
|
|
42
|
+
/** 是否带 UTF-8 BOM(EF BB BF) */
|
|
43
|
+
bom: boolean
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** 已解码文本的形态探测结果(合并侧用:只如实描述,不做任何改写)。 */
|
|
47
|
+
export interface TextForm extends TextShape {
|
|
48
|
+
/** 这份文本里**出现过**的行尾风格(同一份文件可能两种都有 ⇒ 长度为 2) */
|
|
49
|
+
eols: TextEol[]
|
|
50
|
+
/** 孤立 CR(其后不是 LF)的个数;> 0 ⇒ 行尾无法用 TextShape 无损表示 */
|
|
51
|
+
loneCr: number
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** 解码结论。ok:false 时调用方必须**零写入**(绝不 mojibake 写回)。 */
|
|
55
|
+
export type DecodeTextResult =
|
|
56
|
+
| { ok: true; text: string; shape: TextShape }
|
|
57
|
+
| { ok: false; reason: 'binary' | 'non-utf8' }
|
|
58
|
+
|
|
59
|
+
/** NUL 探测窗口(与 line-diff / rollback-point 同口径:只看前 8 KiB) */
|
|
60
|
+
const NUL_PROBE_BYTES = 8192
|
|
61
|
+
|
|
62
|
+
/** UTF-8 BOM 字节 */
|
|
63
|
+
const UTF8_BOM = new Uint8Array([0xef, 0xbb, 0xbf])
|
|
64
|
+
|
|
65
|
+
/** 解码器:fatal = 非法 UTF-8 直接抛错(不替换成 U+FFFD);ignoreBOM = 不代我们剥 BOM(剥与不剥由本模块决定)。 */
|
|
66
|
+
const UTF8_DECODER = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true })
|
|
67
|
+
|
|
68
|
+
const UTF8_ENCODER = new TextEncoder()
|
|
69
|
+
|
|
70
|
+
/** 前 8 KiB 是否出现 NUL 字节(与既有 binary 判据一致) */
|
|
71
|
+
function containsNul(bytes: Uint8Array): boolean {
|
|
72
|
+
const head = bytes.subarray(0, NUL_PROBE_BYTES)
|
|
73
|
+
for (let i = 0; i < head.length; i += 1) {
|
|
74
|
+
if (head[i] === 0) return true
|
|
75
|
+
}
|
|
76
|
+
return false
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** 是否以 UTF-8 BOM(EF BB BF)开头 */
|
|
80
|
+
function startsWithUtf8Bom(bytes: Uint8Array): boolean {
|
|
81
|
+
return bytes.length >= 3 && bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** 逐字节相等(保真自证用) */
|
|
85
|
+
function bytesEqual(left: Uint8Array, right: Uint8Array): boolean {
|
|
86
|
+
if (left.length !== right.length) return false
|
|
87
|
+
for (let i = 0; i < left.length; i += 1) {
|
|
88
|
+
if (left[i] !== right[i]) return false
|
|
89
|
+
}
|
|
90
|
+
return true
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 文本能否无损编码成 UTF-8 字节。
|
|
95
|
+
* 唯一会失败的形态是**孤立代理项**(TextEncoder 会把它替换成 U+FFFD ⇒ 写回即内容损毁)。
|
|
96
|
+
*/
|
|
97
|
+
export function isUtf8RoundTrippable(text: string): boolean {
|
|
98
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
99
|
+
const code = text.charCodeAt(i)
|
|
100
|
+
if (code >= 0xd800 && code <= 0xdbff) {
|
|
101
|
+
// 高代理项后面必须紧跟低代理项,否则是孤立代理项
|
|
102
|
+
const next = text.charCodeAt(i + 1)
|
|
103
|
+
if (!(next >= 0xdc00 && next <= 0xdfff)) return false
|
|
104
|
+
i += 1
|
|
105
|
+
continue
|
|
106
|
+
}
|
|
107
|
+
if (code >= 0xdc00 && code <= 0xdfff) return false
|
|
108
|
+
}
|
|
109
|
+
return true
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** 扫描行尾:完整 CRLF / 纯 LF / 孤立 CR 各计数(三者互斥) */
|
|
113
|
+
function scanEol(text: string): { crlf: number; lf: number; loneCr: number } {
|
|
114
|
+
let crlf = 0
|
|
115
|
+
let lf = 0
|
|
116
|
+
let loneCr = 0
|
|
117
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
118
|
+
const code = text.charCodeAt(i)
|
|
119
|
+
if (code === 13) {
|
|
120
|
+
if (text.charCodeAt(i + 1) === 10) {
|
|
121
|
+
crlf += 1
|
|
122
|
+
i += 1
|
|
123
|
+
} else {
|
|
124
|
+
loneCr += 1
|
|
125
|
+
}
|
|
126
|
+
continue
|
|
127
|
+
}
|
|
128
|
+
if (code === 10) lf += 1
|
|
129
|
+
}
|
|
130
|
+
return { crlf, lf, loneCr }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* 探测一段**已解码文本**的形态:主导行尾、BOM 前缀、出现过的行尾风格、孤立 CR 计数。
|
|
135
|
+
* 纯描述、零改写(shape.eol 只用来决定「新建行」的行尾)。
|
|
136
|
+
*/
|
|
137
|
+
export function probeTextForm(text: string): TextForm {
|
|
138
|
+
const bom = text.startsWith('\uFEFF')
|
|
139
|
+
const body = bom ? text.slice(1) : text
|
|
140
|
+
const scan = scanEol(body)
|
|
141
|
+
const eols: TextEol[] = []
|
|
142
|
+
if (scan.crlf > 0) eols.push('\r\n')
|
|
143
|
+
if (scan.lf > 0) eols.push('\n')
|
|
144
|
+
return {
|
|
145
|
+
// 严格多者胜:相等 / 没有整行行尾 / 空文本 ⇒ '\n'
|
|
146
|
+
eol: scan.crlf > scan.lf ? '\r\n' : '\n',
|
|
147
|
+
bom,
|
|
148
|
+
eols,
|
|
149
|
+
loneCr: scan.loneCr,
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* 字节 → 文本(**无损或拒绝**)。
|
|
155
|
+
*
|
|
156
|
+
* 自证(判据 1):成功时一定有 encodeTextBytes(result.text, result.shape) **逐字节等于**入参 bytes。
|
|
157
|
+
*
|
|
158
|
+
* 拒绝的两种形态(调用方**必须零写入**并按 reason 如实报告):
|
|
159
|
+
* · 'binary':前 8 KiB 含 NUL(不是文本,行 / 合并算法对它没有定义);
|
|
160
|
+
* · 'non-utf8':非法 UTF-8,或解码结果无法逐字节往返,或含孤立 CR(超出两种 EOL 的表示能力)。
|
|
161
|
+
*/
|
|
162
|
+
export function decodeTextBytes(bytes: Uint8Array): DecodeTextResult {
|
|
163
|
+
if (containsNul(bytes)) return { ok: false, reason: 'binary' }
|
|
164
|
+
const bom = startsWithUtf8Bom(bytes)
|
|
165
|
+
const body = bom ? bytes.subarray(UTF8_BOM.length) : bytes
|
|
166
|
+
let text: string
|
|
167
|
+
try {
|
|
168
|
+
text = UTF8_DECODER.decode(body)
|
|
169
|
+
} catch {
|
|
170
|
+
return { ok: false, reason: 'non-utf8' }
|
|
171
|
+
}
|
|
172
|
+
// 自证:解出来的文本必须能逐字节编码回原字节(防「解码器接受了、编码器却写不回」的边角形态)
|
|
173
|
+
if (!bytesEqual(UTF8_ENCODER.encode(text), body)) return { ok: false, reason: 'non-utf8' }
|
|
174
|
+
const form = probeTextForm(text)
|
|
175
|
+
// 孤立 CR:TextShape 表达不了 ⇒ 宁可拒绝,也绝不悄悄把它改成 LF
|
|
176
|
+
if (form.loneCr > 0) return { ok: false, reason: 'non-utf8' }
|
|
177
|
+
return { ok: true, text, shape: { eol: form.eol, bom } }
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* 文本 → 字节(**保真**)。
|
|
182
|
+
*
|
|
183
|
+
* 契约:text 必须是 decodeTextBytes 输出的形态(BOM 已剥)。本函数**不改写任何行尾**,只在
|
|
184
|
+
* shape.bom 为真时补回 3 字节 BOM ⇒ 与 decodeTextBytes 互逆(判据 1)。
|
|
185
|
+
*
|
|
186
|
+
* 注意:shape.eol 在这里**不参与**字节生成 —— 行尾归一化只发生在三方合并里;在这里归一化就意味着
|
|
187
|
+
* 混合行尾 / CRLF 文件无法逐字节往返。含孤立代理项的文本按 UTF-8 替换字符写出(与 TextEncoder 一致):
|
|
188
|
+
* 调用方应先在 decodeTextBytes / mergeThreeWay 处把好关。
|
|
189
|
+
*/
|
|
190
|
+
export function encodeTextBytes(text: string, shape: TextShape): Uint8Array {
|
|
191
|
+
const body = UTF8_ENCODER.encode(text)
|
|
192
|
+
if (!shape.bom) return body
|
|
193
|
+
const out = new Uint8Array(body.length + UTF8_BOM.length)
|
|
194
|
+
out.set(UTF8_BOM, 0)
|
|
195
|
+
out.set(body, UTF8_BOM.length)
|
|
196
|
+
return out
|
|
197
|
+
}
|
package/src/core/types.ts
CHANGED
|
@@ -248,9 +248,17 @@ export interface PluginInfo {
|
|
|
248
248
|
|
|
249
249
|
export interface PluginsFacade {
|
|
250
250
|
listInstalled(): Promise<PluginInfo[]>;
|
|
251
|
-
/**
|
|
252
|
-
*
|
|
253
|
-
|
|
251
|
+
/**
|
|
252
|
+
* 安装/更新插件。`target` 是**已经推导好的完整安装目标**(`name@1.2.3`、`github:user/repo`、
|
|
253
|
+
* `file:/abs/path.tgz`),由 `core/plugin-cli.ts` 的 `pluginInstallTarget()` 统一决策 ——
|
|
254
|
+
* 门面只负责执行,不再二次推导(否则「执行日志里写的」与「真正跑的命令」会分叉,issue #101)。
|
|
255
|
+
* 缺省按裸包名装 npm 最新版(旧调用方 / 旧备份)。
|
|
256
|
+
*
|
|
257
|
+
* `installedVersion`:安装后回读 `node_modules/<pkg>/package.json` 的实际版本(读不到为 null)。
|
|
258
|
+
* 调用方据此校验升级是否真的落地,而不是「子进程返回 0 就当成功」(issue #101)。
|
|
259
|
+
* signal 可选:中止时宿主应 kill 子进程并清理半装状态(恢复 package.json + 删 node_modules/<pkg>)。
|
|
260
|
+
*/
|
|
261
|
+
install(pkg: string, target?: string, signal?: AbortSignal): Promise<{ needsRestart: boolean; installedVersion?: string | null }>;
|
|
254
262
|
}
|
|
255
263
|
|
|
256
264
|
export interface WorkspaceFacade {
|
package/src/core/update-check.ts
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* 不读代理设置(Node 的 fetch 默认不认 HTTP_PROXY/HTTPS_PROXY):公司网络下可能直接失败 ——
|
|
18
18
|
* 这时如实报「检查失败」,其余功能不受影响(本模块与插件主干零耦合)。
|
|
19
19
|
*/
|
|
20
|
-
import { compareVersionStrings } from './validator.ts'
|
|
20
|
+
import { compareVersionStrings, isConcreteVersion } from './validator.ts'
|
|
21
21
|
|
|
22
22
|
/** 被检查的 npm 包名(= package.json 的 name;单点常量,勿在别处再写一份) */
|
|
23
23
|
export const PLUGIN_NPM_PACKAGE = 'dsh-config-manager'
|
|
@@ -51,9 +51,6 @@ export type PluginUpdateResult =
|
|
|
51
51
|
| { ok: true; info: PluginUpdateInfo; cached: boolean }
|
|
52
52
|
| { ok: false; current: string; error: string }
|
|
53
53
|
|
|
54
|
-
/** semver 形态判定(允许 1.2.3 / 1.2.3-rc.1 / 1.2.3+build;不允许多余字符) */
|
|
55
|
-
const SEMVER_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/
|
|
56
|
-
|
|
57
54
|
/**
|
|
58
55
|
* 解析 registry 的 latest 响应体(只认 `{ version: 'x.y.z' }`)。
|
|
59
56
|
* 畸形 / 缺字段 / 版本号不是 semver 形态 → null(调用方据此报「响应异常」,不猜)。
|
|
@@ -63,7 +60,7 @@ export function parseLatestVersion(payload: unknown): string | null {
|
|
|
63
60
|
const version = (payload as Record<string, unknown>)['version']
|
|
64
61
|
if (typeof version !== 'string') return null
|
|
65
62
|
const trimmed = version.trim()
|
|
66
|
-
return
|
|
63
|
+
return isConcreteVersion(trimmed) ? trimmed : null
|
|
67
64
|
}
|
|
68
65
|
|
|
69
66
|
/**
|
package/src/core/validator.ts
CHANGED
|
@@ -49,6 +49,20 @@ export function compareVersionStrings(a: string, b: string): -1 | 0 | 1 {
|
|
|
49
49
|
return compareVersions(parseVersion(a), parseVersion(b));
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/**
|
|
53
|
+
* semver 形态判定(`1.2.3` / `1.2.3-rc.1` / `1.2.3+build`;区间 `^1.2.3`、dist-tag 与空串一律 false)。
|
|
54
|
+
*
|
|
55
|
+
* 与上面的 `compareVersionStrings` 分工不同:那个是**容错比较**(为了让 `0.2.1` 与 `0.2.1-rc.2`
|
|
56
|
+
* 也能排出大小,解析用宽松正则),这个是**严格形态判定** —— 只有它是具体版本时才允许拿来
|
|
57
|
+
* 拼 `<pkg>@<version>` 精确安装(issue #101)。导出为唯一实现,`self-update.ts`(是否接受目标
|
|
58
|
+
* 版本)与 `update-check.ts`(registry 返回值是否认得)都必须复用它,不许各留一份正则。
|
|
59
|
+
*/
|
|
60
|
+
export function isConcreteVersion(v: string): boolean {
|
|
61
|
+
return SEMVER_RE.test(v.trim());
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const SEMVER_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
65
|
+
|
|
52
66
|
/**
|
|
53
67
|
* 兼容性判定的**结构化原因**(与 `computeCompatibility` 同源:评分由本函数派生)。
|
|
54
68
|
*
|