dsh-deps 0.0.1 → 0.1.0

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/plan.mjs ADDED
@@ -0,0 +1,205 @@
1
+ /**
2
+ * 建议顺序 + 体检汇总 + why 解释。全部只读,不写文件。
3
+ *
4
+ * 排序判据(写死):
5
+ * 1) 先 patch,再 minor,最后 major;
6
+ * 2) 同组内按「被依赖次数」降序,次数相同按名字升序;
7
+ * 3) blocked(engines 不兼容 / 当前版本被 yanked)不进入升级队列;
8
+ * 4) unknown(快照缺失 / 范围不可判定)不猜、不排队,单列。
9
+ */
10
+
11
+ import { RISK_RANK, RISK_LABEL, BUMPS } from './classify.mjs'
12
+
13
+ /** 信号严重度排序(选「最该先看」的建议动作用)。 */
14
+ const SEVERITY_RANK = { blocked: 4, high: 3, medium: 2, info: 1, unknown: 0 }
15
+
16
+ /** 升级阶段的先后(写死)。 */
17
+ export const PHASES = ['patch', 'minor', 'major']
18
+
19
+ /** 阶段说明。 */
20
+ export const PHASE_LABEL = { patch: 'patch(同 minor 内)', minor: 'minor(同 major 内)', major: 'major(跨大版本)' }
21
+
22
+ /** 排序理由(给报告与 README 共用)。 */
23
+ export const ORDER_RATIONALE = '先 patch 后 minor 再 major:小步先行,每一步都能单独测试与回滚,出问题容易二分定位;同组内按被依赖次数降序:被依赖越多,升级收益越大,也越早暴露兼容问题,先升它能让后续升级更安全。blocked 不进队列,unknown 不猜。'
24
+
25
+ /**
26
+ * 排序比较器。
27
+ * @param {object} a 条目 a
28
+ * @param {object} b 条目 b
29
+ * @returns {number} 比较结果
30
+ */
31
+ function compareForOrder(a, b) {
32
+ const phase = PHASES.indexOf(a.bump) - PHASES.indexOf(b.bump)
33
+ if (phase !== 0) return phase
34
+ if (b.dependents !== a.dependents) return b.dependents - a.dependents
35
+ return a.name.localeCompare(b.name)
36
+ }
37
+
38
+ /**
39
+ * 生成升级计划。
40
+ * @param {object[]} entries 分类后的条目
41
+ * @param {{countSource?:string}} [options] 选项
42
+ * @returns {object} 计划
43
+ */
44
+ export function buildPlan(entries, options = {}) {
45
+ const actionable = entries.filter((entry) => PHASES.includes(entry.bump) && entry.risk !== 'blocked')
46
+ const blocked = entries.filter((entry) => entry.risk === 'blocked')
47
+ const unknown = entries.filter((entry) => entry.bump === 'unknown' && entry.risk !== 'blocked')
48
+ const pinned = entries.filter((entry) => entry.bump === 'pinned')
49
+
50
+ const sorted = actionable.slice().sort(compareForOrder)
51
+ const order = sorted.map((entry, index) => ({
52
+ step: index + 1,
53
+ name: entry.name,
54
+ bump: entry.bump,
55
+ risk: entry.risk,
56
+ current: entry.current,
57
+ currentVersion: entry.currentVersion,
58
+ latest: entry.latest,
59
+ suggested: entry.suggested,
60
+ dependents: entry.dependents,
61
+ group: entry.bump,
62
+ orderReason: `${entry.bump} 组第 ${sorted.filter((item) => item.bump === entry.bump).indexOf(entry) + 1} 个:被 ${entry.dependents} 个包依赖${entry.risk === 'high' ? ',但属于高风险升级,建议单独 PR' : ''}`,
63
+ }))
64
+
65
+ const groups = {
66
+ patch: order.filter((item) => item.bump === 'patch'),
67
+ minor: order.filter((item) => item.bump === 'minor'),
68
+ major: order.filter((item) => item.bump === 'major'),
69
+ }
70
+
71
+ const counts = {
72
+ total: entries.length,
73
+ actionable: actionable.length,
74
+ byBump: Object.fromEntries(BUMPS.map((bump) => [bump, entries.filter((entry) => entry.bump === bump).length])),
75
+ byRisk: Object.fromEntries(Object.keys(RISK_RANK).map((risk) => [risk, entries.filter((entry) => entry.risk === risk).length])),
76
+ }
77
+
78
+ return {
79
+ order,
80
+ groups,
81
+ blocked: blocked.map(summarize),
82
+ unknown: unknown.map(summarize),
83
+ pinned: pinned.map(summarize),
84
+ rationale: ORDER_RATIONALE,
85
+ phaseOrder: [...PHASES],
86
+ counts,
87
+ countSource: options.countSource ?? 'single-manifest',
88
+ }
89
+ }
90
+
91
+ /**
92
+ * 报告里用的条目摘要(不带 signals 细节)。
93
+ * @param {object} entry 条目
94
+ * @returns {object} 摘要
95
+ */
96
+ function summarize(entry) {
97
+ return {
98
+ name: entry.name,
99
+ bump: entry.bump,
100
+ risk: entry.risk,
101
+ current: entry.current,
102
+ currentVersion: entry.currentVersion,
103
+ latest: entry.latest,
104
+ dependents: entry.dependents,
105
+ reason: entry.reason,
106
+ action: (entry.signals.slice().sort((a, b) => SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity]).find((signal) => signal.action)?.action) ?? null,
107
+ }
108
+ }
109
+
110
+ /**
111
+ * 一次性体检汇总。
112
+ * @param {object[]} entries 条目
113
+ * @param {object} plan 计划
114
+ * @param {object[]} problems 数据问题
115
+ * @param {{nodeVersion?:string, snapshotProvided?:boolean}} [context] 上下文
116
+ * @returns {object} 体检结果
117
+ */
118
+ export function auditReport(entries, plan, problems = [], context = {}) {
119
+ const counts = plan.counts
120
+ const serious = entries.filter((entry) => entry.risk === 'high' || entry.risk === 'blocked')
121
+ const mi = entries.filter((entry) => entry.bump === 'minor')
122
+ const verdict = entries.length === 0 ? 'empty'
123
+ : serious.length > 0 ? 'risk'
124
+ : mi.length > 0 || counts.byBump.unknown > 0 ? 'warn'
125
+ : 'ok'
126
+
127
+ const signals = {}
128
+ for (const entry of entries) {
129
+ for (const signal of entry.signals) signals[signal.signal] = (signals[signal.signal] ?? 0) + 1
130
+ }
131
+
132
+ return {
133
+ verdict,
134
+ counts,
135
+ signals,
136
+ nodeVersion: context.nodeVersion ?? null,
137
+ snapshotProvided: context.snapshotProvided === true,
138
+ summary: summarizeVerdict(verdict, counts, serious.length),
139
+ recommendations: recommendationsFor(entries, plan, problems),
140
+ problems,
141
+ }
142
+ }
143
+
144
+ /**
145
+ * 结论一句话。
146
+ * @param {string} verdict 结论
147
+ * @param {object} counts 计数
148
+ * @param {number} serious 高风险数
149
+ * @returns {string} 文案
150
+ */
151
+ function summarizeVerdict(verdict, counts, serious) {
152
+ if (verdict === 'empty') return '没有依赖需要规划。'
153
+ if (verdict === 'risk') return `有 ${serious} 个高风险/被阻塞项:先处理它们,再按建议顺序升其余 ${counts.actionable} 个。`
154
+ if (verdict === 'warn') return `没有高危项,但有 ${counts.byBump.minor} 个 minor 或 ${counts.byBump.unknown} 个 unknown;按建议顺序小步升。`
155
+ return `只有低风险升级(${counts.actionable} 个可升),按建议顺序执行即可。`
156
+ }
157
+
158
+ /**
159
+ * 建议动作清单。
160
+ * @param {object[]} entries 条目
161
+ * @param {object} plan 计划
162
+ * @param {object[]} problems 数据问题
163
+ * @returns {string[]} 建议
164
+ */
165
+ function recommendationsFor(entries, plan, problems) {
166
+ const list = []
167
+ if (plan.blocked.length > 0) list.push(`先处理 ${plan.blocked.length} 个被阻塞项(engines 不兼容 / 当前版本被 yanked):${plan.blocked.map((item) => item.name).join('、')}`)
168
+ if (plan.counts.byBump.unknown > 0) list.push(`补 ${plan.counts.byBump.unknown} 个 unknown 项的快照(npm view <name> version 写进本地快照),不要凭印象升`)
169
+ if (plan.counts.byBump.major > 0) list.push(`major 升级共 ${plan.counts.byBump.major} 个:单独 PR、读 CHANGELOG、准备回滚`)
170
+ if (plan.order.length > 0) list.push(`按 ${plan.order.length} 步顺序执行;每步升完跑测试再进下一步`)
171
+ if (problems.length > 0) list.push(`先修数据问题:${problems.slice(0, 3).join(';')}`)
172
+ return list
173
+ }
174
+
175
+ /**
176
+ * why:解释一个依赖为什么是这个判定。
177
+ * @param {string} name 依赖名
178
+ * @param {object[]} entries 条目
179
+ * @param {object} context 上下文
180
+ * @param {object} context.snapshot 快照 Map
181
+ * @param {object} context.installed installed Map
182
+ * @param {string} context.nodeVersion Node 版本
183
+ * @param {object} context.manifest 根 manifest
184
+ * @returns {object} 解释
185
+ */
186
+ export function explainEntry(name, entries, context) {
187
+ const entry = entries.find((item) => item.name === name)
188
+ if (!entry) return { found: false, name, reason: `依赖清单里没有 ${name}`, checks: [], conclusion: `依赖清单里没有 ${name}:它不是这份 package.json 的依赖,无从判定。` }
189
+ const snapshotEntry = context.snapshot.get(name)
190
+ const checks = [
191
+ { id: 'declared-range', value: entry.current, detail: `package.json 声明 ${entry.section}: ${entry.name} = ${entry.current}` },
192
+ { id: 'current-version', value: entry.currentVersion, detail: entry.versionSource === 'installed' ? '来自 installed 快照' : entry.versionSource === 'range-floor' ? '取自声明范围下界(不是 lock 里的实际版本)' : '无法确定' },
193
+ { id: 'snapshot', value: snapshotEntry ? snapshotEntry.latest : null, detail: snapshotEntry ? `本地快照 latest=${snapshotEntry.latest ?? '(缺失)'}` : '本地快照里没有这个包' },
194
+ { id: 'bump', value: entry.bump, detail: entry.reason },
195
+ { id: 'risk', value: entry.risk, detail: RISK_LABEL[entry.risk] ?? entry.risk },
196
+ { id: 'signals', value: entry.signals.map((signal) => signal.signal), detail: entry.signals.map((signal) => signal.detail).join(';') || '没有触发风险信号' },
197
+ ]
198
+ if (entry.peer) checks.push({ id: 'peer', value: entry.current, detail: `这是 peerDependency:升级要同时确认宿主支持,并同步 peer 范围` })
199
+ const conclusion = entry.bump === 'unknown'
200
+ ? `未判定:${entry.reason}`
201
+ : entry.bump === 'pinned'
202
+ ? `无需升级:${entry.reason}`
203
+ : `判定为 ${entry.bump}(风险 ${entry.risk}):${entry.reason}`
204
+ return { found: true, name, entry, checks, conclusion }
205
+ }
@@ -0,0 +1,270 @@
1
+ /**
2
+ * semver 子集:只做「升级规划」需要的解析、比较、范围判定与范围下界。
3
+ *
4
+ * 明确支持:`1.2.3` / `^1.2.3` / `~1.2.3` / `>=1.2.3` / `>1.2.3` / `<=` / `<` /
5
+ * 空格连接的 AND 范围 / `||` OR / `1.x` / `1.2.x` / `*` / 连字符 `1.2.3 - 2.3.4`。
6
+ *
7
+ * **明确不支持**(调用方会看到 unknown,而不是猜):
8
+ * `npm:` 别名、`git:`/`github:`、`file:`、`link:`、`workspace:`、`latest` 标签、
9
+ * 以及 npm 的 prerelease 特例(`^1.2.3` 不自动含 `1.3.0-beta`)。
10
+ */
11
+
12
+ /** 版本解析失败。 */
13
+ export class SemverError extends Error {
14
+ /**
15
+ * @param {string} message 说明
16
+ */
17
+ constructor(message) {
18
+ super(message)
19
+ this.name = 'SemverError'
20
+ }
21
+ }
22
+
23
+ /** 非 semver 的范围前缀:这些一律 unknown,不去猜。 */
24
+ export const UNSUPPORTED_RANGE_PREFIXES = ['npm:', 'git:', 'git+', 'github:', 'file:', 'link:', 'workspace:', 'http:', 'https:', 'portal:', 'patch:']
25
+
26
+ /**
27
+ * 解析版本。
28
+ * @param {unknown} input 版本字符串
29
+ * @returns {{major:number, minor:number, patch:number, prerelease:string[], raw:string}|null} 解析结果
30
+ */
31
+ export function parseVersion(input) {
32
+ if (typeof input !== 'string') return null
33
+ const text = input.trim().replace(/^v/i, '')
34
+ const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(text)
35
+ if (!match) return null
36
+ return {
37
+ major: Number(match[1]),
38
+ minor: Number(match[2]),
39
+ patch: Number(match[3]),
40
+ prerelease: match[4] ? match[4].split('.') : [],
41
+ raw: text,
42
+ }
43
+ }
44
+
45
+ /**
46
+ * 比较两个版本。
47
+ * @param {object|string} a 版本 a
48
+ * @param {object|string} b 版本 b
49
+ * @returns {number} -1 / 0 / 1
50
+ */
51
+ export function compareVersions(a, b) {
52
+ const left = typeof a === 'string' ? parseVersion(a) : a
53
+ const right = typeof b === 'string' ? parseVersion(b) : b
54
+ if (!left || !right) throw new SemverError(`无法比较版本:${String(a)} vs ${String(b)}`)
55
+ for (const key of ['major', 'minor', 'patch']) {
56
+ if (left[key] !== right[key]) return left[key] > right[key] ? 1 : -1
57
+ }
58
+ const leftPre = left.prerelease ?? []
59
+ const rightPre = right.prerelease ?? []
60
+ if (leftPre.length === 0 && rightPre.length === 0) return 0
61
+ if (leftPre.length === 0) return 1
62
+ if (rightPre.length === 0) return -1
63
+ for (let i = 0; i < Math.max(leftPre.length, rightPre.length); i++) {
64
+ const x = leftPre[i]
65
+ const y = rightPre[i]
66
+ if (x === undefined) return -1
67
+ if (y === undefined) return 1
68
+ const nx = /^\d+$/.test(x)
69
+ const ny = /^\d+$/.test(y)
70
+ if (nx && ny) {
71
+ if (Number(x) !== Number(y)) return Number(x) > Number(y) ? 1 : -1
72
+ } else if (nx !== ny) {
73
+ return nx ? -1 : 1
74
+ } else if (x !== y) {
75
+ return x > y ? 1 : -1
76
+ }
77
+ }
78
+ return 0
79
+ }
80
+
81
+ /**
82
+ * 版本是否严格相等。
83
+ * @param {string} a 版本 a
84
+ * @param {string} b 版本 b
85
+ * @returns {boolean} 是否相等
86
+ */
87
+ export function versionEquals(a, b) {
88
+ const left = parseVersion(a)
89
+ const right = parseVersion(b)
90
+ return Boolean(left && right && compareVersions(left, right) === 0)
91
+ }
92
+
93
+ /**
94
+ * 解析一个比较器(`>=1.2.3` / `^1.2.3` / `1.2.x` …)。
95
+ * @param {string} token 片段
96
+ * @returns {object|null} 比较器
97
+ */
98
+ function parseComparator(token) {
99
+ const text = token.trim()
100
+ if (text === '' || text === '*' || text === 'x' || text === 'X') return null
101
+ const match = /^(>=|<=|>|<|=|\^|~)?\s*v?(\d+|[x*])(?:\.(\d+|[x*]))?(?:\.(\d+|[x*]))?(?:-([0-9A-Za-z.-]+))?$/.exec(text)
102
+ if (!match) return undefined
103
+ const op = match[1] ?? ''
104
+ const wild = (part) => part === undefined || part === 'x' || part === 'X' || part === '*'
105
+ const nums = [match[2], match[3], match[4]]
106
+ if (wild(nums[0])) return null
107
+ const major = Number(nums[0])
108
+ const minor = wild(nums[1]) ? null : Number(nums[1])
109
+ const patch = wild(nums[2]) ? null : Number(nums[2])
110
+ const prerelease = match[5] ? match[5].split('.') : []
111
+
112
+ if (op === '^') {
113
+ if (minor === null) return { op: 'range', from: { major, minor: 0, patch: 0 }, to: { major: major + 1, minor: 0, patch: 0 } }
114
+ if (patch === null) return { op: 'range', from: { major, minor, patch: 0 }, to: { major: major + 1, minor: 0, patch: 0 } }
115
+ if (major > 0) return { op: 'range', from: { major, minor, patch }, to: { major: major + 1, minor: 0, patch: 0 } }
116
+ if (minor > 0) return { op: 'range', from: { major, minor, patch }, to: { major: 0, minor: minor + 1, patch: 0 } }
117
+ return { op: 'range', from: { major, minor, patch }, to: { major: 0, minor: 0, patch: patch + 1 } }
118
+ }
119
+
120
+ if (op === '~' || (op === '' && (minor === null || patch === null))) {
121
+ if (minor === null) return { op: 'range', from: { major, minor: 0, patch: 0 }, to: { major: major + 1, minor: 0, patch: 0 } }
122
+ return { op: 'range', from: { major, minor, patch: patch ?? 0 }, to: { major, minor: minor + 1, patch: 0 } }
123
+ }
124
+
125
+ if (op === '' || op === '=') {
126
+ if (patch === null) {
127
+ return minor === null
128
+ ? { op: 'range', from: { major, minor: 0, patch: 0 }, to: { major: major + 1, minor: 0, patch: 0 } }
129
+ : { op: 'range', from: { major, minor, patch: 0 }, to: { major, minor: minor + 1, patch: 0 } }
130
+ }
131
+ return { op: 'eq', version: { major, minor, patch, prerelease } }
132
+ }
133
+
134
+ const version = { major, minor: minor ?? 0, patch: patch ?? 0, prerelease }
135
+ return { op, version }
136
+ }
137
+
138
+ /**
139
+ * 判断版本是否落在单个比较器集合(AND)里。
140
+ * @param {object} version 版本
141
+ * @param {Array<object|null>} comparators 比较器
142
+ * @returns {boolean} 是否满足
143
+ */
144
+ function satisfiesSet(version, comparators) {
145
+ for (const comparator of comparators) {
146
+ if (comparator === null) continue
147
+ if (comparator.op === 'eq') {
148
+ if (compareVersions(version, comparator.version) !== 0) return false
149
+ } else if (comparator.op === 'range') {
150
+ if (compareVersions(version, comparator.from) < 0) return false
151
+ if (compareVersions(version, comparator.to) >= 0) return false
152
+ } else if (comparator.op === '>=') {
153
+ if (compareVersions(version, comparator.version) < 0) return false
154
+ } else if (comparator.op === '>') {
155
+ if (compareVersions(version, comparator.version) <= 0) return false
156
+ } else if (comparator.op === '<=') {
157
+ if (compareVersions(version, comparator.version) > 0) return false
158
+ } else if (comparator.op === '<') {
159
+ if (compareVersions(version, comparator.version) >= 0) return false
160
+ } else {
161
+ return false
162
+ }
163
+ }
164
+ return true
165
+ }
166
+
167
+ /**
168
+ * 解析范围;不支持或不认识就返回 null(调用方标 unknown)。
169
+ * @param {unknown} range 范围字符串
170
+ * @returns {{sets:Array<Array<object>>}|null} 解析结果
171
+ */
172
+ export function parseRange(range) {
173
+ if (typeof range !== 'string') return null
174
+ const text = range.trim()
175
+ if (text.length === 0) return null
176
+ const lower = text.toLowerCase()
177
+ if (UNSUPPORTED_RANGE_PREFIXES.some((prefix) => lower.startsWith(prefix))) return null
178
+ if (lower === 'latest' || lower === 'next' || lower === 'beta') return null
179
+
180
+ const sets = []
181
+ for (const part of text.split('||')) {
182
+ const trimmed = part.trim()
183
+ if (trimmed === '') return null
184
+ // 连字符范围
185
+ const hyphen = /^(\S+)\s+-\s+(\S+)$/.exec(trimmed)
186
+ if (hyphen) {
187
+ const from = parseComparator(`>=${hyphen[1]}`)
188
+ const to = parseComparator(`<=${hyphen[2]}`)
189
+ if (!from || !to) return null
190
+ sets.push([from, to])
191
+ continue
192
+ }
193
+ const comparators = []
194
+ for (const token of trimmed.split(/\s+/)) {
195
+ const comparator = parseComparator(token)
196
+ if (comparator === undefined) return null
197
+ comparators.push(comparator)
198
+ }
199
+ sets.push(comparators)
200
+ }
201
+ return { sets }
202
+ }
203
+
204
+ /**
205
+ * 版本是否满足范围。
206
+ * @param {string} version 版本
207
+ * @param {string} range 范围
208
+ * @returns {{supported:boolean, satisfies:boolean, reason?:string}} 判定
209
+ */
210
+ export function satisfiesRange(version, range) {
211
+ const parsedVersion = parseVersion(version)
212
+ if (!parsedVersion) return { supported: false, satisfies: false, reason: `版本不可解析:${version}` }
213
+ const parsedRange = parseRange(range)
214
+ if (!parsedRange) return { supported: false, satisfies: false, reason: `范围不可解析:${range}` }
215
+ const satisfies = parsedRange.sets.some((set) => satisfiesSet(parsedVersion, set))
216
+ return { supported: true, satisfies }
217
+ }
218
+
219
+ /**
220
+ * 范围下界(能代表「当前声明」的最早版本);无从判断返回 null。
221
+ * @param {unknown} range 范围
222
+ * @returns {string|null} 下界版本
223
+ */
224
+ export function rangeFloor(range) {
225
+ const parsed = parseRange(range)
226
+ if (!parsed) return null
227
+ let best = null
228
+ for (const set of parsed.sets) {
229
+ const froms = set.filter((comparator) => comparator && (comparator.op === 'range' || comparator.op === '>=' || comparator.op === '>' || comparator.op === 'eq'))
230
+ for (const comparator of froms) {
231
+ const version = comparator.op === 'eq' ? comparator.version : comparator.version ?? comparator.from
232
+ if (!version) continue
233
+ if (best === null || compareVersions(version, best) < 0) best = version
234
+ }
235
+ }
236
+ if (best === null) return null
237
+ return `${best.major}.${best.minor}.${best.patch}${best.prerelease?.length ? `-${best.prerelease.join('.')}` : ''}`
238
+ }
239
+
240
+ /**
241
+ * 升级类型:比较两个版本给 patch / minor / major。
242
+ * @param {string} from 当前
243
+ * @param {string} to 目标
244
+ * @returns {'patch'|'minor'|'major'|'same'} 升级类型
245
+ */
246
+ export function bumpType(from, to) {
247
+ const left = parseVersion(from)
248
+ const right = parseVersion(to)
249
+ if (!left || !right) return 'same'
250
+ if (compareVersions(left, right) === 0) return 'same'
251
+ if (left.major !== right.major) return 'major'
252
+ if (left.minor !== right.minor) return 'minor'
253
+ return 'patch'
254
+ }
255
+
256
+ /**
257
+ * 判断引擎范围是否与当前 Node 兼容。
258
+ * @param {unknown} engines package.json 的 engines 字段
259
+ * @param {string} nodeVersion 当前 Node 版本
260
+ * @returns {{declared:boolean, compatible:boolean|null, range?:string, reason?:string}} 判定
261
+ */
262
+ export function checkEngines(engines, nodeVersion) {
263
+ if (engines === null || engines === undefined) return { declared: false, compatible: null }
264
+ if (typeof engines !== 'object' || Array.isArray(engines)) return { declared: true, compatible: null, reason: 'engines 不是对象' }
265
+ const range = engines.node
266
+ if (typeof range !== 'string' || range.trim().length === 0) return { declared: false, compatible: null }
267
+ const result = satisfiesRange(nodeVersion, range)
268
+ if (!result.supported) return { declared: true, compatible: null, range, reason: `engines.node 范围不可判定:${range}` }
269
+ return { declared: true, compatible: result.satisfies, range }
270
+ }
package/package.json CHANGED
@@ -1,24 +1,52 @@
1
1
  {
2
2
  "name": "dsh-deps",
3
- "version": "0.0.1",
4
- "description": "Audit dependency trees for license risk, dupes, abandoned packages and version drift, with an actionable upgrade plan per package. | 审计依赖树:许可证风险、重复包、无人维护包、版本漂移,并按包给出可执行的升级计划。",
3
+ "version": "0.1.0",
4
+ "description": "依赖升级规划 DSH 插件:给定 package.json 与本地 registry 快照,算哪些依赖能升、升了有什么风险、按什么顺序升。五档分级写死(patch / minor / major / pinned / unknown);latest 只来自调用方给的本地快照,缺了就 unknown、绝不联网猜;风险信号具体到 major / engines 不兼容 / deprecated / yanked / peer 破坏,每条给建议动作;建议顺序先 patch 后 minor 再 major、同组按被依赖次数降序并说明理由;只读、不自动改文件(pin 只返回 diff 文本)。零运行时依赖,含 CLI。 | Dependency upgrade planner for DeepSeek Harness: five fixed bump classes, latest only from a caller-provided local snapshot (never network, never guessed), concrete risk signals with actions, and an upgrade order (patch → minor → major, dependents first) with rationale. Read-only: never edits package.json or lockfiles; pin returns a diff only.",
5
5
  "keywords": [
6
6
  "dsh",
7
7
  "dsh-plugin",
8
8
  "deepseek-harness",
9
9
  "dependencies",
10
- "license",
11
- "audit",
12
10
  "upgrade",
11
+ "audit",
12
+ "semver",
13
13
  "supply-chain",
14
- "依赖"
14
+ "依赖",
15
+ "升级"
15
16
  ],
16
17
  "license": "MIT",
18
+ "author": "he fangsheng (https://github.com/dongsheng123132)",
17
19
  "type": "module",
20
+ "main": "./plugin.mjs",
21
+ "bin": {
22
+ "dsh-deps": "./lib/cli.js"
23
+ },
24
+ "exports": {
25
+ ".": "./plugin.mjs",
26
+ "./cli": "./lib/cli.js",
27
+ "./lib/*": "./lib/*",
28
+ "./package.json": "./package.json"
29
+ },
18
30
  "files": [
19
- "README.md"
31
+ "plugin.mjs",
32
+ "lib/",
33
+ "cordis.patch.yml",
34
+ "dsh-plugin.naming.json",
35
+ "README.md",
36
+ "LICENSE"
20
37
  ],
38
+ "dsh": {
39
+ "bundle": {
40
+ "patch": "./cordis.patch.yml"
41
+ }
42
+ },
21
43
  "engines": {
22
- "node": ">=22"
44
+ "node": ">=22",
45
+ "dsh": ">=0.2.2-rc.1 <0.3.0"
46
+ },
47
+ "scripts": {
48
+ "test": "node --test test/*.test.mjs",
49
+ "test:dir": "node --test test/",
50
+ "check": "node --check plugin.mjs && node scripts/check.mjs"
23
51
  }
24
52
  }