dsh-m 0.9.23 → 0.9.25
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 +28 -0
- package/README.en.md +2 -2
- package/README.md +2 -2
- package/docs/DESIGN.md +5 -3
- package/lib/client.js +118 -75
- package/lib/core/dsh-cli.js +98 -31
- package/lib/core/exclude-governance.js +264 -0
- package/lib/core/market.js +5 -0
- package/lib/core/profile-ops.js +124 -19
- package/lib/core/registry.js +11 -0
- package/lib/core/release-age.js +72 -96
- package/package.json +1 -1
- package/registry.json +3 -3
package/lib/core/release-age.js
CHANGED
|
@@ -1,35 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* 供应链等待期(minimumReleaseAge)预检与失败翻译(0.9.19 立基,0.9.x 按治理语义收敛,ADR-0009)。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* operation-
|
|
6
|
-
* 1.
|
|
7
|
-
* `minimumReleaseAgeExclude`
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
4
|
+
* 实证基准(2026-10-03,本机 desktop profile,pnpm 11.7.0,官方管理器操作日志
|
|
5
|
+
* operation-zsc4ei / operation-L8oq1z / operation-l5fXEd):
|
|
6
|
+
* 1. 默认策略(未显式设置 age、未开 strict = 非严格)下,显式点名的「太新」目标
|
|
7
|
+
* 直接放行安装,pnpm 自动向 pnpm-workspace.yaml 的 `minimumReleaseAgeExclude`
|
|
8
|
+
* 追加 `pkg@ver` 条目("Added 1 entry …");
|
|
9
|
+
* 2. pnpm 的 evaluateVersionPolicy **每包名只认第一条排除规则**,同名后续规则死亡
|
|
10
|
+
* (dshmarket #732 实证,首条规则生效机理)——死规则会让等待期内全部包操作被
|
|
11
|
+
* 锁文件级校验拦死(0:15 quota-watch 连坐实录);
|
|
12
12
|
* 3. 校验失败前目标包可能已写入 node_modules,官方管理器只回滚 manifest/lockfile
|
|
13
|
-
*
|
|
13
|
+
* →「账实分裂」,下次包操作静默回退。
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
* ① 委派前预检(releaseAgePrecheck
|
|
17
|
-
*
|
|
15
|
+
* 职责(ADR-0009):
|
|
16
|
+
* ① 委派前预检(releaseAgePrecheck):仅「显式设置 age 或 strict 开启」时对窗口内
|
|
17
|
+
* 目标拒绝(附可重试时刻);默认策略放行并返回陈述性 notice。锁内坏形态由
|
|
18
|
+
* exclude-governance 的治理挂点处理,本模块不再预拒绝(0.9.19 ②腿退役)。
|
|
18
19
|
* ② 失败翻译(describeReleaseAgeFailure):解析全部违规条目,按「本次目标 vs
|
|
19
|
-
*
|
|
20
|
+
* 旁包」分述发布时刻与可重试时刻。
|
|
21
|
+
* ③ cachedPackumentTimes:预检与登记共用的带缓存 packument 探测。
|
|
20
22
|
*
|
|
21
|
-
* fail-open 纪律:发布时刻不可得 / 策略文件不可读 / profileDir 缺席 /
|
|
22
|
-
*
|
|
23
|
+
* fail-open 纪律:发布时刻不可得 / 策略文件不可读 / profileDir 缺席 / 命中任一
|
|
24
|
+
* 排除规则(首条规则口径)→ 一律放行;pnpm 仍是最终执行者。
|
|
23
25
|
*/
|
|
24
26
|
import { readFile } from 'node:fs/promises';
|
|
25
27
|
import { join } from 'node:path';
|
|
26
28
|
import { isMap, isScalar, isSeq, parseDocument } from 'yaml';
|
|
27
29
|
import { fetchJsonLimited } from './httpx.js';
|
|
28
|
-
import { isExactVersion } from './versions.js';
|
|
29
30
|
/** pnpm 11.7 实机测得的策略窗口:24h(operation-l5fXEd 的 cutoff = 检查时刻 − 1440min)。 */
|
|
30
31
|
export const DEFAULT_MINIMUM_RELEASE_AGE_MIN = 1440;
|
|
31
|
-
/** 锁内「独立精确排除条目」旁包探测上限(防清单失控;超出部分放弃预判,交 pnpm 兜底)。 */
|
|
32
|
-
export const MAX_EXCLUDE_PROBES = 8;
|
|
33
32
|
function scalarNumber(value) {
|
|
34
33
|
if (typeof value === 'number' && Number.isFinite(value))
|
|
35
34
|
return value;
|
|
@@ -43,7 +42,7 @@ function scalarNumber(value) {
|
|
|
43
42
|
* 调用方按默认窗口 + 无排除处理,绝不抛。
|
|
44
43
|
*/
|
|
45
44
|
export function parseWorkspacePolicy(text) {
|
|
46
|
-
const empty = { minimumReleaseAgeMin: null, excludes: [] };
|
|
45
|
+
const empty = { explicitAge: false, minimumReleaseAgeMin: null, strict: false, excludes: [] };
|
|
47
46
|
let document;
|
|
48
47
|
try {
|
|
49
48
|
document = parseDocument(text);
|
|
@@ -62,10 +61,17 @@ export function parseWorkspacePolicy(text) {
|
|
|
62
61
|
}
|
|
63
62
|
}
|
|
64
63
|
}
|
|
65
|
-
// yaml 库的 Map.get() 对标量返回已解包的原始值(number/string),对集合返回节点——两种形态都兜住
|
|
64
|
+
// yaml 库的 Map.get() 对标量返回已解包的原始值(number/string/boolean),对集合返回节点——两种形态都兜住
|
|
66
65
|
const rawAge = document.contents.get('minimumReleaseAge');
|
|
67
66
|
const age = isScalar(rawAge) ? scalarNumber(rawAge.value) : scalarNumber(rawAge);
|
|
68
|
-
|
|
67
|
+
const rawStrict = document.contents.get('minimumReleaseAgeStrict');
|
|
68
|
+
const strict = rawStrict === true || (isScalar(rawStrict) && rawStrict.value === true);
|
|
69
|
+
return {
|
|
70
|
+
explicitAge: rawAge !== undefined && rawAge !== null,
|
|
71
|
+
minimumReleaseAgeMin: age !== null && age > 0 ? age : null,
|
|
72
|
+
strict,
|
|
73
|
+
excludes,
|
|
74
|
+
};
|
|
69
75
|
}
|
|
70
76
|
export function excludeMatch(selector, pkg, version) {
|
|
71
77
|
const s = selector.trim();
|
|
@@ -73,7 +79,7 @@ export function excludeMatch(selector, pkg, version) {
|
|
|
73
79
|
return 'no';
|
|
74
80
|
if (s.includes('||')) {
|
|
75
81
|
for (const part of s.split('||')) {
|
|
76
|
-
if (excludeMatch(part.trim(), pkg, version)
|
|
82
|
+
if (excludeMatch(part.trim(), pkg, version) === 'effective')
|
|
77
83
|
return 'effective';
|
|
78
84
|
}
|
|
79
85
|
return 'no';
|
|
@@ -83,30 +89,7 @@ export function excludeMatch(selector, pkg, version) {
|
|
|
83
89
|
// 包名级(dshmarket)或裸版本号段(只应出现在复合条目内,顶层出现视为包名比较)
|
|
84
90
|
return s === pkg || s === version ? 'effective' : 'no';
|
|
85
91
|
}
|
|
86
|
-
|
|
87
|
-
const verPart = s.slice(at + 1);
|
|
88
|
-
if (namePart !== pkg)
|
|
89
|
-
return 'no';
|
|
90
|
-
if (verPart === version) {
|
|
91
|
-
return pkg.startsWith('@') ? 'effective' : 'unreliable-unscoped-exact';
|
|
92
|
-
}
|
|
93
|
-
return 'no';
|
|
94
|
-
}
|
|
95
|
-
/** 从独立精确条目提取 (pkg, version);复合/包名级/版本段非精确 semver → null。 */
|
|
96
|
-
export function splitExactSelector(selector) {
|
|
97
|
-
const s = selector.trim();
|
|
98
|
-
if (s === '' || s.includes('||'))
|
|
99
|
-
return null;
|
|
100
|
-
const at = s.lastIndexOf('@');
|
|
101
|
-
if (at <= 0)
|
|
102
|
-
return null;
|
|
103
|
-
const pkg = s.slice(0, at);
|
|
104
|
-
const version = s.slice(at + 1);
|
|
105
|
-
if (!isExactVersion(version))
|
|
106
|
-
return null;
|
|
107
|
-
if (!/^@?[A-Za-z0-9-._~]+(\/[A-Za-z0-9-._~]+)?$/.test(pkg))
|
|
108
|
-
return null;
|
|
109
|
-
return { pkg, version, reliable: pkg.startsWith('@') };
|
|
92
|
+
return s.slice(0, at) === pkg && s.slice(at + 1) === version ? 'effective' : 'no';
|
|
110
93
|
}
|
|
111
94
|
const VIOLATION_RE = /(\S+@\S+) was published at ([^,]+), within the minimumReleaseAge cutoff \(([^)]+)\)/g;
|
|
112
95
|
/** 解析 pnpm 诊断里的全部等待期违规条目(0.9.10 只取第一条是「报错报错包」的根因之一)。 */
|
|
@@ -196,72 +179,65 @@ export async function readWorkspacePolicy(profileDir) {
|
|
|
196
179
|
}
|
|
197
180
|
const packumentCache = new Map();
|
|
198
181
|
const PACKUMENT_CACHE_TTL_MS = 5 * 60_000;
|
|
182
|
+
/**
|
|
183
|
+
* 带进程内缓存的 packument time 探测(TTL 5min):预检与登记(exclude-governance)
|
|
184
|
+
* 共用——同一操作里预检刚拉过的 packument 不二次拉取(8MiB 级全量)。
|
|
185
|
+
*/
|
|
186
|
+
export async function cachedPackumentTimes(pkg, timeoutMs = 12_000, signal) {
|
|
187
|
+
const hit = packumentCache.get(pkg);
|
|
188
|
+
if (hit && Date.now() - hit.at < PACKUMENT_CACHE_TTL_MS)
|
|
189
|
+
return hit.times;
|
|
190
|
+
const times = await npmPackumentTimes(pkg, timeoutMs, signal);
|
|
191
|
+
packumentCache.set(pkg, { at: Date.now(), times });
|
|
192
|
+
return times;
|
|
193
|
+
}
|
|
199
194
|
/** 测试钩子:清空 packument 进程内缓存。 */
|
|
200
195
|
export function _resetReleaseAgeCachesForTests() {
|
|
201
196
|
packumentCache.clear();
|
|
202
197
|
}
|
|
203
198
|
/**
|
|
204
|
-
*
|
|
205
|
-
*
|
|
199
|
+
* 委派前预检(ADR-0009 收敛语义):仅当策略显式设置 age 或开启 strict 时,对窗口内
|
|
200
|
+
* 且未被任一排除规则覆盖(首条规则口径)的目标结构化拒绝(附可重试时刻);默认
|
|
201
|
+
* 策略(非严格)放行并返回陈述性 notice——官方管理器对显式点名的新版本会直接
|
|
202
|
+
* 安装并自动登记,锁内坏形态由 exclude-governance 的治理挂点处理。零写操作;
|
|
203
|
+
* fail-open:发布时刻不可得 / profileDir 缺席 / 命中排除规则 → 放行。
|
|
206
204
|
*/
|
|
207
205
|
export async function releaseAgePrecheck(input) {
|
|
208
|
-
// profileDir 缺席 =
|
|
206
|
+
// profileDir 缺席 = 读不到操作员排除条目与策略 → 不预判
|
|
209
207
|
if (!input.profileDir)
|
|
210
208
|
return { blocked: false };
|
|
211
209
|
const now = input.nowMs ?? Date.now();
|
|
212
210
|
const policy = input.deps?.workspacePolicy
|
|
213
211
|
? await input.deps.workspacePolicy(input.profileDir)
|
|
214
212
|
: await readWorkspacePolicy(input.profileDir);
|
|
215
|
-
const
|
|
213
|
+
const explicit = policy?.explicitAge ?? false;
|
|
214
|
+
const strict = policy?.strict ?? false;
|
|
215
|
+
const policyMin = explicit && policy?.minimumReleaseAgeMin != null ? policy.minimumReleaseAgeMin : DEFAULT_MINIMUM_RELEASE_AGE_MIN;
|
|
216
216
|
const probe = async (pkg) => {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
const times = await (input.deps?.packumentTimes ?? npmPackumentTimes)(pkg, Math.min(input.timeoutMs ?? 20_000, 12_000), input.signal);
|
|
221
|
-
packumentCache.set(pkg, { at: Date.now(), times });
|
|
222
|
-
return times;
|
|
217
|
+
if (input.deps?.packumentTimes)
|
|
218
|
+
return input.deps.packumentTimes(pkg, Math.min(input.timeoutMs ?? 20_000, 12_000), input.signal);
|
|
219
|
+
return cachedPackumentTimes(pkg, 12_000, input.signal);
|
|
223
220
|
};
|
|
224
|
-
const blockers = [];
|
|
225
|
-
// ① 本次目标版本的发布时刻(有效排除条目覆盖 → 放行)
|
|
226
221
|
const times = await probe(input.pkg);
|
|
227
222
|
const publishedAt = times ? Date.parse(times[input.version] ?? '') : NaN;
|
|
228
|
-
if (Number.isFinite(publishedAt))
|
|
229
|
-
const exempt = (policy?.excludes ?? []).some((sel) => excludeMatch(sel, input.pkg, input.version) === 'effective');
|
|
230
|
-
if (!exempt) {
|
|
231
|
-
const deadline = publishedAt + policyMin * 60_000;
|
|
232
|
-
if (now < deadline) {
|
|
233
|
-
blockers.push({ entry: `${input.pkg}@${input.version}`, role: 'target', publishedAt, deadline });
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
// ② 锁内「不被校验认可的独立精确排除条目」(非 scoped)——任何一条未满期都会拦死整次安装
|
|
238
|
-
const seen = new Set([`${input.pkg}@${input.version}`]);
|
|
239
|
-
const candidates = (policy?.excludes ?? [])
|
|
240
|
-
.map((sel) => splitExactSelector(sel))
|
|
241
|
-
.filter((x) => x !== null && !x.reliable);
|
|
242
|
-
for (const ex of candidates.slice(0, MAX_EXCLUDE_PROBES)) {
|
|
243
|
-
const entry = `${ex.pkg}@${ex.version}`;
|
|
244
|
-
if (seen.has(entry))
|
|
245
|
-
continue;
|
|
246
|
-
seen.add(entry);
|
|
247
|
-
const t2 = await probe(ex.pkg);
|
|
248
|
-
const pub = t2 ? Date.parse(t2[ex.version] ?? '') : NaN;
|
|
249
|
-
if (!Number.isFinite(pub))
|
|
250
|
-
continue;
|
|
251
|
-
const deadline = pub + policyMin * 60_000;
|
|
252
|
-
if (now < deadline) {
|
|
253
|
-
blockers.push({ entry, role: 'lockfile-exclude', publishedAt: pub, deadline });
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
if (blockers.length === 0)
|
|
223
|
+
if (!Number.isFinite(publishedAt))
|
|
257
224
|
return { blocked: false };
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
225
|
+
const exempt = (policy?.excludes ?? []).some((sel) => excludeMatch(sel, input.pkg, input.version) === 'effective');
|
|
226
|
+
const deadline = publishedAt + policyMin * 60_000;
|
|
227
|
+
if (exempt || now >= deadline)
|
|
228
|
+
return { blocked: false };
|
|
229
|
+
if (!explicit && !strict) {
|
|
230
|
+
return {
|
|
231
|
+
blocked: false,
|
|
232
|
+
notice: `目标 ${input.pkg}@${input.version} 发布于 ${fmtLocal(publishedAt)}(等待期内):官方管理器将按显式点名安装,装好后 dsh-m 会登记豁免条目`,
|
|
233
|
+
};
|
|
263
234
|
}
|
|
264
|
-
const
|
|
265
|
-
lines.push(
|
|
266
|
-
|
|
235
|
+
const lines = ['官方管理器的供应链策略(minimumReleaseAge)将拦截本次安装,已提前拒绝(未触碰 profile 文件):'];
|
|
236
|
+
lines.push(`· 目标 ${input.pkg}@${input.version}:发布于 ${fmtLocal(publishedAt)},预计 ${fmtLocal(deadline)} 后满等待期`);
|
|
237
|
+
lines.push(`预计 ${fmtLocal(deadline)} 后可重试;等待期内也可由操作员修正 pnpm-workspace.yaml 的排除条目形态(包名级/复合)解除拦截`);
|
|
238
|
+
return {
|
|
239
|
+
blocked: true,
|
|
240
|
+
message: lines.join('\n'),
|
|
241
|
+
blockers: [{ entry: `${input.pkg}@${input.version}`, role: 'target', publishedAt, deadline }],
|
|
242
|
+
};
|
|
267
243
|
}
|
package/package.json
CHANGED
package/registry.json
CHANGED
|
@@ -99,10 +99,10 @@
|
|
|
99
99
|
{
|
|
100
100
|
"id": "dsh-copilot-auth",
|
|
101
101
|
"name": "DSH Copilot Auth",
|
|
102
|
-
"description": "
|
|
102
|
+
"description": "内置 Copilot 登录注销页与开箱路由;目录兜底不覆盖定制,桌面版不支持目录刷新。已适配 0.1.7-rc.2 与 0.2.0-rc.1/rc.2。",
|
|
103
103
|
"category": "self-dev",
|
|
104
104
|
"tags": ["Copilot", "认证", "模型接入", "工具"],
|
|
105
|
-
"verified": ["0.1.7-rc.2", "0.2.0-rc.1"],
|
|
105
|
+
"verified": ["0.1.7-rc.2", "0.2.0-rc.1", "0.2.0-rc.2"],
|
|
106
106
|
"source": "npm",
|
|
107
107
|
"npm": "@inventec/dsh-copilot-auth",
|
|
108
108
|
"github": "iasiv5/dsh-copilot-auth",
|
|
@@ -114,7 +114,7 @@
|
|
|
114
114
|
"description": "GLM 与 Copilot 额度悬浮球:胶囊、详情面板与侧栏卡片,宿主解析凭据,适配 web 与 desktop。",
|
|
115
115
|
"category": "self-dev",
|
|
116
116
|
"tags": ["配额", "悬浮球", "GLM", "Copilot"],
|
|
117
|
-
"verified": ["0.1.7-rc.2", "0.
|
|
117
|
+
"verified": ["0.1.7-rc.2", "0.2.0-rc.2"],
|
|
118
118
|
"source": "npm",
|
|
119
119
|
"npm": "@iasiv5/dsh-quota-watch",
|
|
120
120
|
"github": "iasiv5/dsh-quota-watch",
|