@chatcode/cco-market 1.45.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/LICENSE +21 -0
- package/README.md +146 -0
- package/README.zh.md +143 -0
- package/UPDATE-API-V1.md +132 -0
- package/client/client.js +11534 -0
- package/cordis.patch.yml +5 -0
- package/lib/accelerate.js +184 -0
- package/lib/agents.js +36 -0
- package/lib/backup.js +572 -0
- package/lib/catalog-local-match.js +126 -0
- package/lib/catalog-npm.js +111 -0
- package/lib/changelog.js +242 -0
- package/lib/channels.js +64 -0
- package/lib/check.js +1067 -0
- package/lib/compatibility.js +198 -0
- package/lib/diagnostics.js +49 -0
- package/lib/discovery-compatibility.js +260 -0
- package/lib/dsh-cli.js +949 -0
- package/lib/dsh-install.js +108 -0
- package/lib/gist.js +352 -0
- package/lib/groups.js +97 -0
- package/lib/home-paths.js +42 -0
- package/lib/hot.js +519 -0
- package/lib/http.js +39 -0
- package/lib/index.js +97 -0
- package/lib/install.js +387 -0
- package/lib/log.js +197 -0
- package/lib/ndjson.js +154 -0
- package/lib/net.js +98 -0
- package/lib/order.js +284 -0
- package/lib/patch.js +521 -0
- package/lib/pnpm-compat.js +490 -0
- package/lib/presets.js +278 -0
- package/lib/profile.js +911 -0
- package/lib/region-probe.js +93 -0
- package/lib/regions.js +253 -0
- package/lib/registry.js +204 -0
- package/lib/restart.js +373 -0
- package/lib/routes.js +4372 -0
- package/lib/settings.js +109 -0
- package/lib/skill-market/api.js +119 -0
- package/lib/skill-market/config.js +29 -0
- package/lib/skill-market/installer.js +352 -0
- package/lib/skill-market/runtime.js +13 -0
- package/lib/skill-market/tui.js +119 -0
- package/lib/skill-market/types.js +1 -0
- package/lib/skill-market/web-routes.js +123 -0
- package/lib/snapshot.js +500 -0
- package/lib/source-migration.js +54 -0
- package/lib/sources.js +596 -0
- package/lib/store.js +91 -0
- package/lib/themes.js +102 -0
- package/lib/trial.js +116 -0
- package/lib/types/accelerate.d.ts +85 -0
- package/lib/types/agents.d.ts +22 -0
- package/lib/types/backup.d.ts +131 -0
- package/lib/types/catalog-local-match.d.ts +50 -0
- package/lib/types/catalog-npm.d.ts +47 -0
- package/lib/types/changelog.d.ts +102 -0
- package/lib/types/channels.d.ts +56 -0
- package/lib/types/check.d.ts +266 -0
- package/lib/types/compatibility.d.ts +64 -0
- package/lib/types/diagnostics.d.ts +29 -0
- package/lib/types/discovery-compatibility.d.ts +80 -0
- package/lib/types/dsh-cli.d.ts +324 -0
- package/lib/types/dsh-install.d.ts +46 -0
- package/lib/types/gist.d.ts +53 -0
- package/lib/types/groups.d.ts +34 -0
- package/lib/types/home-paths.d.ts +16 -0
- package/lib/types/hot.d.ts +221 -0
- package/lib/types/http.d.ts +12 -0
- package/lib/types/index.d.ts +14 -0
- package/lib/types/install.d.ts +172 -0
- package/lib/types/log.d.ts +43 -0
- package/lib/types/ndjson.d.ts +52 -0
- package/lib/types/net.d.ts +58 -0
- package/lib/types/order.d.ts +100 -0
- package/lib/types/patch.d.ts +129 -0
- package/lib/types/pnpm-compat.d.ts +93 -0
- package/lib/types/presets.d.ts +90 -0
- package/lib/types/profile.d.ts +253 -0
- package/lib/types/region-probe.d.ts +50 -0
- package/lib/types/regions.d.ts +122 -0
- package/lib/types/registry.d.ts +86 -0
- package/lib/types/restart.d.ts +194 -0
- package/lib/types/routes.d.ts +67 -0
- package/lib/types/settings.d.ts +78 -0
- package/lib/types/skill-market/api.d.ts +10 -0
- package/lib/types/skill-market/config.d.ts +3 -0
- package/lib/types/skill-market/installer.d.ts +23 -0
- package/lib/types/skill-market/runtime.d.ts +10 -0
- package/lib/types/skill-market/tui.d.ts +4 -0
- package/lib/types/skill-market/types.d.ts +71 -0
- package/lib/types/skill-market/web-routes.d.ts +19 -0
- package/lib/types/snapshot.d.ts +93 -0
- package/lib/types/source-migration.d.ts +11 -0
- package/lib/types/sources.d.ts +216 -0
- package/lib/types/store.d.ts +30 -0
- package/lib/types/themes.d.ts +40 -0
- package/lib/types/trial.d.ts +61 -0
- package/lib/types/update-api-v1.d.ts +66 -0
- package/lib/types/updates.d.ts +123 -0
- package/lib/types/verify.d.ts +139 -0
- package/lib/update-api-v1.js +215 -0
- package/lib/updates.js +361 -0
- package/lib/verify.js +453 -0
- package/package.json +121 -0
- package/src/accelerate.ts +213 -0
- package/src/agents.ts +43 -0
- package/src/backup.ts +583 -0
- package/src/catalog-local-match.ts +144 -0
- package/src/catalog-npm.ts +120 -0
- package/src/changelog.ts +282 -0
- package/src/channels.ts +70 -0
- package/src/check.ts +1239 -0
- package/src/client/CommentsModal.tsx +119 -0
- package/src/client/Diagnostics.tsx +907 -0
- package/src/client/ErrorBoundary.tsx +111 -0
- package/src/client/InstallToast.tsx +31 -0
- package/src/client/Market.module.css +830 -0
- package/src/client/MarketSection.tsx +5301 -0
- package/src/client/OperationsPanel.tsx +365 -0
- package/src/client/SettingsCard.tsx +621 -0
- package/src/client/SkillMarket.module.css +21 -0
- package/src/client/SkillMarketSection.tsx +162 -0
- package/src/client/comments.ts +54 -0
- package/src/client/globals.d.ts +13 -0
- package/src/client/index.ts +180 -0
- package/src/client/locales.ts +1112 -0
- package/src/client/market-data.ts +1321 -0
- package/src/client/operations.ts +201 -0
- package/src/client/preset-panel.tsx +263 -0
- package/src/client/primitives.d.ts +140 -0
- package/src/client/self-check.ts +147 -0
- package/src/client/snapshot-panel.tsx +244 -0
- package/src/compatibility.ts +237 -0
- package/src/diagnostics.ts +84 -0
- package/src/discovery-compatibility.ts +315 -0
- package/src/dsh-cli.ts +1126 -0
- package/src/dsh-install.ts +118 -0
- package/src/gist.ts +362 -0
- package/src/groups.ts +111 -0
- package/src/home-paths.ts +53 -0
- package/src/hot.ts +628 -0
- package/src/http.ts +41 -0
- package/src/index.ts +128 -0
- package/src/install.ts +420 -0
- package/src/log.ts +206 -0
- package/src/ndjson.ts +185 -0
- package/src/net.ts +106 -0
- package/src/order.ts +303 -0
- package/src/patch.ts +522 -0
- package/src/pnpm-compat.ts +527 -0
- package/src/presets.ts +344 -0
- package/src/profile.ts +940 -0
- package/src/region-probe.ts +97 -0
- package/src/regions.ts +310 -0
- package/src/registry.ts +250 -0
- package/src/restart.ts +396 -0
- package/src/routes.ts +4506 -0
- package/src/settings.ts +141 -0
- package/src/skill-market/api.ts +130 -0
- package/src/skill-market/config.ts +32 -0
- package/src/skill-market/installer.ts +337 -0
- package/src/skill-market/runtime.ts +14 -0
- package/src/skill-market/tui.ts +157 -0
- package/src/skill-market/types.ts +80 -0
- package/src/skill-market/web-routes.ts +132 -0
- package/src/snapshot.ts +532 -0
- package/src/source-migration.ts +61 -0
- package/src/sources.ts +565 -0
- package/src/store.ts +89 -0
- package/src/themes.ts +125 -0
- package/src/trial.ts +156 -0
- package/src/update-api-v1.ts +277 -0
- package/src/updates.ts +400 -0
- package/src/verify.ts +492 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-contract compatibility preflight for #195.
|
|
3
|
+
*
|
|
4
|
+
* Pure evaluation of what `analyzeProfile()` already reports: a confirmed
|
|
5
|
+
* peer mismatch (`satisfied === false`) is translated into a directional
|
|
6
|
+
* verdict:
|
|
7
|
+
*
|
|
8
|
+
* - `belowMin`: the resolved version is older than every alternative's lower
|
|
9
|
+
* bound — the environment is too old for the plugin's declared contract.
|
|
10
|
+
* - `aboveMax`: the resolved version is newer than every alternative's upper
|
|
11
|
+
* bound (or the exact pin). This is only a risk when the author expressed
|
|
12
|
+
* an explicit upper bound or exact pin; otherwise it is a warning, because
|
|
13
|
+
* the ecosystem currently has many sloppy `^0.0.1`-style declarations that
|
|
14
|
+
* work in practice.
|
|
15
|
+
*
|
|
16
|
+
* Everything else stays informational: `*`, prerelease-vs-`*` artifacts,
|
|
17
|
+
* unparseable ranges, and optional peers never produce a risk here.
|
|
18
|
+
*/
|
|
19
|
+
import { analyzeProfile, compareSemver } from './check.js';
|
|
20
|
+
import { profileDir, readInstalledManifest } from './profile.js';
|
|
21
|
+
const SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
22
|
+
function parseComparator(part) {
|
|
23
|
+
const p = part.trim();
|
|
24
|
+
const m = /^(\^|~|>=|<=|>|<)?(.*)$/.exec(p);
|
|
25
|
+
if (m === null)
|
|
26
|
+
return null;
|
|
27
|
+
const target = (m[2] ?? '').trim();
|
|
28
|
+
if (!SEMVER.test(target))
|
|
29
|
+
return null;
|
|
30
|
+
const raw = m[1] ?? '';
|
|
31
|
+
return { op: raw === '' ? 'exact' : raw, target };
|
|
32
|
+
}
|
|
33
|
+
/** Next breaking bump for `^` / `~`, using `-0` so prereleases compare consistently. */
|
|
34
|
+
function nextBound(target, kind) {
|
|
35
|
+
const m = /^(\d+)\.(\d+)\.(\d+)(?:-[0-9A-Za-z.-]+)?$/.exec(target);
|
|
36
|
+
if (m === null)
|
|
37
|
+
return null;
|
|
38
|
+
const major = Number(m[1]);
|
|
39
|
+
const minor = Number(m[2]);
|
|
40
|
+
const patch = Number(m[3]);
|
|
41
|
+
if (kind === '^') {
|
|
42
|
+
if (major > 0)
|
|
43
|
+
return `${major + 1}.0.0-0`;
|
|
44
|
+
if (minor > 0)
|
|
45
|
+
return `0.${minor + 1}.0-0`;
|
|
46
|
+
return `0.0.${patch + 1}-0`;
|
|
47
|
+
}
|
|
48
|
+
return `${major}.${minor + 1}.0-0`;
|
|
49
|
+
}
|
|
50
|
+
function boundsFor(range) {
|
|
51
|
+
const alternatives = [];
|
|
52
|
+
for (const rawAlternative of range.split('||')) {
|
|
53
|
+
const parts = rawAlternative.trim().split(/\s+/).filter(Boolean);
|
|
54
|
+
const parsed = parts.map(parseComparator);
|
|
55
|
+
if (parsed.some(part => part === null))
|
|
56
|
+
return null;
|
|
57
|
+
let lower = null;
|
|
58
|
+
let upper = null;
|
|
59
|
+
let explicitUpper = false;
|
|
60
|
+
let exact = null;
|
|
61
|
+
for (const part of parsed) {
|
|
62
|
+
const p = part;
|
|
63
|
+
if (p.op === 'exact') {
|
|
64
|
+
exact = p.target;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (p.op === '^' || p.op === '~') {
|
|
68
|
+
const bound = nextBound(p.target, p.op);
|
|
69
|
+
if (bound === null)
|
|
70
|
+
return null;
|
|
71
|
+
if (lower === null || compareSemver(p.target, lower.target) > 0)
|
|
72
|
+
lower = p;
|
|
73
|
+
if (upper === null || compareSemver(bound, upper.target) < 0)
|
|
74
|
+
upper = { op: '<=', target: bound };
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (p.op === '>=' || p.op === '>') {
|
|
78
|
+
if (lower === null || compareSemver(p.target, lower.target) > 0)
|
|
79
|
+
lower = p;
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
if (upper === null || compareSemver(p.target, upper.target) < 0)
|
|
83
|
+
upper = p;
|
|
84
|
+
explicitUpper = true;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
alternatives.push({ lower, upper, explicitUpper, exact });
|
|
88
|
+
}
|
|
89
|
+
return alternatives;
|
|
90
|
+
}
|
|
91
|
+
function belowAllMins(resolved, bounds) {
|
|
92
|
+
return bounds.every(alternative => {
|
|
93
|
+
if (alternative.exact !== null)
|
|
94
|
+
return compareSemver(resolved, alternative.exact) < 0;
|
|
95
|
+
if (alternative.lower === null)
|
|
96
|
+
return false;
|
|
97
|
+
const lower = alternative.lower;
|
|
98
|
+
return lower.op === '>' ? compareSemver(resolved, lower.target) <= 0 : compareSemver(resolved, lower.target) < 0;
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
function aboveAllMaxes(resolved, bounds) {
|
|
102
|
+
return bounds.every(alternative => {
|
|
103
|
+
if (alternative.exact !== null)
|
|
104
|
+
return compareSemver(resolved, alternative.exact) > 0;
|
|
105
|
+
if (alternative.upper === null)
|
|
106
|
+
return false;
|
|
107
|
+
const upper = alternative.upper;
|
|
108
|
+
return upper.op === '<' ? compareSemver(resolved, upper.target) >= 0 : compareSemver(resolved, upper.target) > 0;
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
function hasExplicitUpperOrExact(bounds) {
|
|
112
|
+
return bounds.every(alternative => alternative.exact !== null || alternative.explicitUpper);
|
|
113
|
+
}
|
|
114
|
+
/** Translate one confirmed peer mismatch into a directional verdict. */
|
|
115
|
+
export function classifyPeer(plugin, peer, range, resolved, optional) {
|
|
116
|
+
if (resolved === null)
|
|
117
|
+
return { kind: 'none' };
|
|
118
|
+
if (optional) {
|
|
119
|
+
return {
|
|
120
|
+
kind: 'warning',
|
|
121
|
+
warning: { plugin, peer, range, resolved, reason: 'optional' },
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const bounds = boundsFor(range);
|
|
125
|
+
if (bounds === null)
|
|
126
|
+
return { kind: 'none' };
|
|
127
|
+
if (belowAllMins(resolved, bounds)) {
|
|
128
|
+
return {
|
|
129
|
+
kind: 'risk',
|
|
130
|
+
risk: { plugin, peer, range, resolved, direction: 'belowMin' },
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
if (aboveAllMaxes(resolved, bounds)) {
|
|
134
|
+
return hasExplicitUpperOrExact(bounds)
|
|
135
|
+
? {
|
|
136
|
+
kind: 'risk',
|
|
137
|
+
risk: { plugin, peer, range, resolved, direction: 'aboveMax' },
|
|
138
|
+
}
|
|
139
|
+
: {
|
|
140
|
+
kind: 'warning',
|
|
141
|
+
warning: { plugin, peer, range, resolved, reason: 'aboveMax' },
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
return { kind: 'none' };
|
|
145
|
+
}
|
|
146
|
+
/** Whether a peer is declared optional in the installed plugin manifest. */
|
|
147
|
+
export function isOptionalPeer(profileDirectory, plugin, peer) {
|
|
148
|
+
const manifest = readInstalledManifest('web', plugin, profileDirectory);
|
|
149
|
+
return manifest?.peerDependenciesMeta?.[peer]?.optional === true;
|
|
150
|
+
}
|
|
151
|
+
/** Evaluate the current profile with the same machinery `/dsh-market/check` uses. */
|
|
152
|
+
export function assessCompatibility(profileDirectory, options) {
|
|
153
|
+
const report = analyzeProfile(profileDirectory, options);
|
|
154
|
+
const risks = [];
|
|
155
|
+
const warnings = [];
|
|
156
|
+
for (const mismatch of report.peerMismatches) {
|
|
157
|
+
if (mismatch.satisfied !== false)
|
|
158
|
+
continue;
|
|
159
|
+
const optional = isOptionalPeer(profileDirectory, mismatch.plugin, mismatch.name);
|
|
160
|
+
const verdict = classifyPeer(mismatch.plugin, mismatch.name, mismatch.range, mismatch.resolved, optional);
|
|
161
|
+
if (verdict.kind === 'risk')
|
|
162
|
+
risks.push(verdict.risk);
|
|
163
|
+
else if (verdict.kind === 'warning')
|
|
164
|
+
warnings.push(verdict.warning);
|
|
165
|
+
}
|
|
166
|
+
return { risks, warnings, duplicateNames: report.duplicateNames };
|
|
167
|
+
}
|
|
168
|
+
function riskId(risk) {
|
|
169
|
+
return `${risk.plugin}\u0000${risk.peer}\u0000${risk.direction}`;
|
|
170
|
+
}
|
|
171
|
+
/** Risks present after a mutation but absent before it. */
|
|
172
|
+
export function introducedRisks(before, after) {
|
|
173
|
+
const seen = new Set(before.risks.map(riskId));
|
|
174
|
+
return after.risks.filter(risk => !seen.has(riskId(risk)));
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Cross-layer name collisions present after a mutation but absent before it
|
|
178
|
+
* (#230 by @dxc-dxc).
|
|
179
|
+
*
|
|
180
|
+
* Keyed by NAME alone, not by the layer set: a collision the operation made
|
|
181
|
+
* worse — same name, now shadowing across one more layer — is still the same
|
|
182
|
+
* collision the profile already had, and re-reporting it would put the
|
|
183
|
+
* operator back in front of a problem they did not just cause.
|
|
184
|
+
*
|
|
185
|
+
* This is what makes surfacing these safe at all. The underlying
|
|
186
|
+
* `duplicateNames` is informational precisely because a healthy-but-messy
|
|
187
|
+
* profile can carry collisions indefinitely; only the newly introduced ones
|
|
188
|
+
* are attributable to the install that just ran, and therefore undoable by
|
|
189
|
+
* rolling it back.
|
|
190
|
+
*/
|
|
191
|
+
export function introducedDuplicateNames(before, after) {
|
|
192
|
+
const seen = new Set(before.duplicateNames.map(entry => entry.name));
|
|
193
|
+
return after.duplicateNames.filter(entry => !seen.has(entry.name));
|
|
194
|
+
}
|
|
195
|
+
/** Convenience wrapper matching the profile helper signature. */
|
|
196
|
+
export function assessProfile(profile, explicitDir) {
|
|
197
|
+
return assessCompatibility(profileDir(profile, explicitDir));
|
|
198
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** Versioned, read-only diagnostics shared by the host route and client. */
|
|
2
|
+
export const DIAGNOSTIC_SCHEMA = 'dsh-market/diagnostics/v1';
|
|
3
|
+
/** Conservative first set of known, identity-sensitive host contracts. */
|
|
4
|
+
export const KNOWN_SHARED_HOST_PACKAGES = [
|
|
5
|
+
'@deepseek-ai/cordis',
|
|
6
|
+
'@deepseek-ai/dsh-attachment',
|
|
7
|
+
'@deepseek-ai/dsh-llm',
|
|
8
|
+
'@deepseek-ai/dsh-system-prompt',
|
|
9
|
+
'@deepseek-ai/dsh-tools',
|
|
10
|
+
];
|
|
11
|
+
const knownSharedHostPackages = new Set(KNOWN_SHARED_HOST_PACKAGES);
|
|
12
|
+
function isRecord(value) {
|
|
13
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
14
|
+
}
|
|
15
|
+
/** Report exact manifest declarations; the resolved dependency tree is not inspected. */
|
|
16
|
+
function inspectKnownHostDependencyDeclarations(packageName, manifest) {
|
|
17
|
+
if (!isRecord(manifest) || !isRecord(manifest.dependencies))
|
|
18
|
+
return [];
|
|
19
|
+
const findings = [];
|
|
20
|
+
for (const dependency of Object.keys(manifest.dependencies).sort()) {
|
|
21
|
+
const declaredRange = manifest.dependencies[dependency];
|
|
22
|
+
if (!knownSharedHostPackages.has(dependency) || typeof declaredRange !== 'string')
|
|
23
|
+
continue;
|
|
24
|
+
findings.push({
|
|
25
|
+
code: 'shared-host-package-dependency',
|
|
26
|
+
severity: 'warning',
|
|
27
|
+
subject: { kind: 'package', name: packageName },
|
|
28
|
+
evidence: {
|
|
29
|
+
basis: 'manifest-declaration',
|
|
30
|
+
dependency,
|
|
31
|
+
declaredRange,
|
|
32
|
+
declaredIn: 'dependencies',
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return findings;
|
|
37
|
+
}
|
|
38
|
+
/** Build a stable diagnostic envelope from installed package manifests. */
|
|
39
|
+
export function diagnosePackageManifests(packages) {
|
|
40
|
+
const sortedPackages = [...packages].sort((a, b) => a.packageName < b.packageName ? -1 : a.packageName > b.packageName ? 1 : 0);
|
|
41
|
+
return {
|
|
42
|
+
schema: DIAGNOSTIC_SCHEMA,
|
|
43
|
+
findings: sortedPackages.flatMap(({ packageName, manifest }) => {
|
|
44
|
+
if (!isRecord(manifest) || manifest.dsh === undefined)
|
|
45
|
+
return [];
|
|
46
|
+
return inspectKnownHostDependencyDeclarations(packageName, manifest);
|
|
47
|
+
}),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discovery-time DSH compatibility metadata.
|
|
3
|
+
*
|
|
4
|
+
* The public catalog does not carry npm manifests. Fetching every manifest
|
|
5
|
+
* while the market opens would turn one catalog request into more than a
|
|
6
|
+
* thousand registry requests, so this module supplies a bounded, on-demand
|
|
7
|
+
* index. Successful public manifest facts are cached beside the market's
|
|
8
|
+
* profile state; conclusions are never cached because they depend on the DSH
|
|
9
|
+
* version of the process serving the page.
|
|
10
|
+
*/
|
|
11
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
12
|
+
import { dirname } from 'node:path';
|
|
13
|
+
import { satisfiesRange } from './check.js';
|
|
14
|
+
import { classifyPeer } from './compatibility.js';
|
|
15
|
+
import { marketFetch } from './net.js';
|
|
16
|
+
const CACHE_SCHEMA = 'dsh-market/discovery-compatibility-cache/v1';
|
|
17
|
+
const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000;
|
|
18
|
+
const FAILURE_COOLDOWN_MS = 5 * 60 * 1000;
|
|
19
|
+
const OUTAGE_COOLDOWN_MS = 30 * 1000;
|
|
20
|
+
const FETCH_TIMEOUT_MS = 8_000;
|
|
21
|
+
const DEFAULT_CONCURRENCY = 8;
|
|
22
|
+
const MAX_CACHE_ENTRIES = 5_000;
|
|
23
|
+
const MAX_RANGE_LENGTH = 256;
|
|
24
|
+
function range(value) {
|
|
25
|
+
if (typeof value !== 'string')
|
|
26
|
+
return null;
|
|
27
|
+
const trimmed = value.trim();
|
|
28
|
+
return trimmed !== '' && trimmed.length <= MAX_RANGE_LENGTH ? trimmed : null;
|
|
29
|
+
}
|
|
30
|
+
function record(value) {
|
|
31
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
32
|
+
? value
|
|
33
|
+
: null;
|
|
34
|
+
}
|
|
35
|
+
/** Keep only the small public subset of an npm manifest needed by discovery. */
|
|
36
|
+
export function manifestFacts(value) {
|
|
37
|
+
const manifest = record(value) ?? {};
|
|
38
|
+
const engines = record(manifest.engines);
|
|
39
|
+
const peers = record(manifest.peerDependencies);
|
|
40
|
+
const peerDependencies = {};
|
|
41
|
+
for (const [name, raw] of Object.entries(peers ?? {})) {
|
|
42
|
+
if (!name.startsWith('@deepseek-ai/'))
|
|
43
|
+
continue;
|
|
44
|
+
const declared = range(raw);
|
|
45
|
+
if (declared !== null)
|
|
46
|
+
peerDependencies[name] = declared;
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
version: range(manifest.version),
|
|
50
|
+
enginesDsh: range(engines?.dsh),
|
|
51
|
+
peerDependencies,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function validFacts(value) {
|
|
55
|
+
const facts = record(value);
|
|
56
|
+
const peers = record(facts?.peerDependencies);
|
|
57
|
+
return facts !== null
|
|
58
|
+
&& (facts.version === null || range(facts.version) === facts.version)
|
|
59
|
+
&& (facts.enginesDsh === null || range(facts.enginesDsh) === facts.enginesDsh)
|
|
60
|
+
&& peers !== null
|
|
61
|
+
&& Object.entries(peers).every(([name, item]) => name.startsWith('@deepseek-ai/') && range(item) === item);
|
|
62
|
+
}
|
|
63
|
+
function displayRequirement(declarations) {
|
|
64
|
+
const unique = [...new Set(declarations.map(item => item.range))];
|
|
65
|
+
return unique.length === 0 ? null : unique.join(' ∩ ');
|
|
66
|
+
}
|
|
67
|
+
function rangeResult(hostVersion, declared) {
|
|
68
|
+
return satisfiesRange(hostVersion, declared, { includePrerelease: true });
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Host peers imply the DSH release-line floor, but old ecosystem packages
|
|
72
|
+
* often carry caret ranges such as `^0.0.1` whose computed 0.x upper bound
|
|
73
|
+
* was never intended as a host ceiling. Reuse install preflight's directional
|
|
74
|
+
* policy: below-min and explicit upper/exact violations are definite; a
|
|
75
|
+
* newer host above an implicit caret/tilde ceiling remains compatible.
|
|
76
|
+
*/
|
|
77
|
+
function declarationResult(declaration, hostVersion) {
|
|
78
|
+
const satisfied = rangeResult(hostVersion, declaration.range);
|
|
79
|
+
if (declaration.kind === 'engine' || satisfied !== false)
|
|
80
|
+
return satisfied;
|
|
81
|
+
const verdict = classifyPeer('discovery', declaration.package ?? '@deepseek-ai/dsh', declaration.range, hostVersion, false);
|
|
82
|
+
if (verdict.kind === 'risk')
|
|
83
|
+
return false;
|
|
84
|
+
if (verdict.kind === 'warning' && verdict.warning.reason === 'aboveMax')
|
|
85
|
+
return true;
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Derive the current host verdict from raw manifest facts.
|
|
90
|
+
*
|
|
91
|
+
* Every valid declaration is conjunctive: an explicit `engines.dsh` and all
|
|
92
|
+
* host peers must agree. A malformed declaration keeps a passing result
|
|
93
|
+
* unknown, but cannot erase a definite mismatch from another declaration.
|
|
94
|
+
*/
|
|
95
|
+
export function deriveHostCompatibility(facts, hostVersion, hostPackages) {
|
|
96
|
+
if (facts === null) {
|
|
97
|
+
return { status: 'unknown', basis: 'unavailable', requirement: null, declarations: [] };
|
|
98
|
+
}
|
|
99
|
+
const declarations = [];
|
|
100
|
+
if (facts.enginesDsh !== null) {
|
|
101
|
+
declarations.push({ kind: 'engine', range: facts.enginesDsh });
|
|
102
|
+
}
|
|
103
|
+
for (const [name, declared] of Object.entries(facts.peerDependencies)) {
|
|
104
|
+
// Cordis (4.x) and schemastery (3.x) are host packages, but are not on
|
|
105
|
+
// DSH's lockstep 0.x release line and therefore say nothing about the
|
|
106
|
+
// DSH version. `hostPackages` is the local install inventory plus its
|
|
107
|
+
// curated fallback, so future lockstep DSH packages are picked up too.
|
|
108
|
+
if (!hostPackages.has(name) || !/^@deepseek-ai\/dsh(?:-|$)/.test(name))
|
|
109
|
+
continue;
|
|
110
|
+
declarations.push({ kind: 'peer', package: name, range: declared });
|
|
111
|
+
}
|
|
112
|
+
const requirement = displayRequirement(declarations);
|
|
113
|
+
if (declarations.length === 0) {
|
|
114
|
+
return { status: 'unknown', basis: 'undeclared', requirement: null, declarations: [] };
|
|
115
|
+
}
|
|
116
|
+
if (hostVersion === null) {
|
|
117
|
+
return { status: 'unknown', basis: 'manifest', requirement, declarations };
|
|
118
|
+
}
|
|
119
|
+
const outcomes = declarations.map(item => declarationResult(item, hostVersion));
|
|
120
|
+
const status = outcomes.some(item => item === false)
|
|
121
|
+
? 'incompatible'
|
|
122
|
+
: outcomes.every(item => item === true)
|
|
123
|
+
? 'compatible'
|
|
124
|
+
: 'unknown';
|
|
125
|
+
return { status, basis: 'manifest', requirement, declarations };
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Durable, bounded lookup of npm `latest` manifests.
|
|
129
|
+
*
|
|
130
|
+
* Failed requests are intentionally memory-only and short-lived: a mirror
|
|
131
|
+
* outage must not become a day-long false "undeclared" result on disk.
|
|
132
|
+
*/
|
|
133
|
+
export class DiscoveryManifestIndex {
|
|
134
|
+
cacheFile;
|
|
135
|
+
entries = new Map();
|
|
136
|
+
failures = new Map();
|
|
137
|
+
inflight = new Map();
|
|
138
|
+
fetcher;
|
|
139
|
+
now;
|
|
140
|
+
ttlMs;
|
|
141
|
+
concurrency;
|
|
142
|
+
loaded = false;
|
|
143
|
+
consecutiveFailures = 0;
|
|
144
|
+
unavailableUntil = 0;
|
|
145
|
+
dirty = false;
|
|
146
|
+
activeFetches = 0;
|
|
147
|
+
fetchWaiters = [];
|
|
148
|
+
constructor(cacheFile, options = {}) {
|
|
149
|
+
this.cacheFile = cacheFile;
|
|
150
|
+
this.fetcher = options.fetcher ?? marketFetch;
|
|
151
|
+
this.now = options.now ?? Date.now;
|
|
152
|
+
this.ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
|
|
153
|
+
this.concurrency = Math.max(1, Math.floor(options.concurrency ?? DEFAULT_CONCURRENCY));
|
|
154
|
+
}
|
|
155
|
+
load() {
|
|
156
|
+
if (this.loaded)
|
|
157
|
+
return;
|
|
158
|
+
this.loaded = true;
|
|
159
|
+
try {
|
|
160
|
+
const parsed = JSON.parse(readFileSync(this.cacheFile, 'utf8'));
|
|
161
|
+
if (parsed.schema !== CACHE_SCHEMA || record(parsed.entries) === null)
|
|
162
|
+
return;
|
|
163
|
+
for (const [name, raw] of Object.entries(parsed.entries)) {
|
|
164
|
+
const entry = record(raw);
|
|
165
|
+
if (entry === null || typeof entry.checkedAt !== 'number' || !validFacts(entry.facts))
|
|
166
|
+
continue;
|
|
167
|
+
this.entries.set(name, { checkedAt: entry.checkedAt, facts: entry.facts });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
catch { /* first run or a truncated cache — fetch fresh */ }
|
|
171
|
+
}
|
|
172
|
+
persist() {
|
|
173
|
+
try {
|
|
174
|
+
mkdirSync(dirname(this.cacheFile), { recursive: true, mode: 0o700 });
|
|
175
|
+
const newest = [...this.entries.entries()]
|
|
176
|
+
.sort(([, a], [, b]) => b.checkedAt - a.checkedAt)
|
|
177
|
+
.slice(0, MAX_CACHE_ENTRIES);
|
|
178
|
+
this.entries.clear();
|
|
179
|
+
for (const [name, entry] of newest)
|
|
180
|
+
this.entries.set(name, entry);
|
|
181
|
+
const data = { schema: CACHE_SCHEMA, entries: Object.fromEntries(newest) };
|
|
182
|
+
writeFileSync(this.cacheFile, JSON.stringify(data), { mode: 0o600 });
|
|
183
|
+
}
|
|
184
|
+
catch { /* cache failure degrades to in-memory lookups */ }
|
|
185
|
+
}
|
|
186
|
+
/** One semaphore for the whole index, including overlapping HTTP batches. */
|
|
187
|
+
async withFetchPermit(operation) {
|
|
188
|
+
if (this.activeFetches >= this.concurrency) {
|
|
189
|
+
await new Promise(resolve => this.fetchWaiters.push(resolve));
|
|
190
|
+
}
|
|
191
|
+
this.activeFetches += 1;
|
|
192
|
+
try {
|
|
193
|
+
return await operation();
|
|
194
|
+
}
|
|
195
|
+
finally {
|
|
196
|
+
this.activeFetches -= 1;
|
|
197
|
+
this.fetchWaiters.shift()?.();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
async fetchOne(name, registry) {
|
|
201
|
+
this.load();
|
|
202
|
+
const now = this.now();
|
|
203
|
+
const cached = this.entries.get(name);
|
|
204
|
+
if (cached !== undefined && now - cached.checkedAt < this.ttlMs)
|
|
205
|
+
return cached.facts;
|
|
206
|
+
if ((this.failures.get(name) ?? 0) > now || this.unavailableUntil > now)
|
|
207
|
+
return null;
|
|
208
|
+
const pending = this.inflight.get(name);
|
|
209
|
+
if (pending !== undefined)
|
|
210
|
+
return await pending;
|
|
211
|
+
const request = (async () => {
|
|
212
|
+
try {
|
|
213
|
+
const response = await this.withFetchPermit(async () => await this.fetcher(`${registry}/${encodeURIComponent(name)}/latest`, {
|
|
214
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
215
|
+
headers: { accept: 'application/json', 'user-agent': 'dsh-market' },
|
|
216
|
+
}));
|
|
217
|
+
if (!response.ok)
|
|
218
|
+
throw new Error(`HTTP ${String(response.status)}`);
|
|
219
|
+
const facts = manifestFacts(await response.json());
|
|
220
|
+
this.entries.set(name, { checkedAt: this.now(), facts });
|
|
221
|
+
this.dirty = true;
|
|
222
|
+
this.failures.delete(name);
|
|
223
|
+
this.consecutiveFailures = 0;
|
|
224
|
+
return facts;
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
const failedAt = this.now();
|
|
228
|
+
this.failures.set(name, failedAt + FAILURE_COOLDOWN_MS);
|
|
229
|
+
this.consecutiveFailures += 1;
|
|
230
|
+
if (this.consecutiveFailures >= this.concurrency) {
|
|
231
|
+
this.unavailableUntil = failedAt + OUTAGE_COOLDOWN_MS;
|
|
232
|
+
}
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
finally {
|
|
236
|
+
this.inflight.delete(name);
|
|
237
|
+
}
|
|
238
|
+
})();
|
|
239
|
+
this.inflight.set(name, request);
|
|
240
|
+
return await request;
|
|
241
|
+
}
|
|
242
|
+
/** Look up a bounded batch while never exceeding the configured fan-out. */
|
|
243
|
+
async lookup(names, registry) {
|
|
244
|
+
const unique = [...new Set(names)];
|
|
245
|
+
const result = {};
|
|
246
|
+
let next = 0;
|
|
247
|
+
const worker = async () => {
|
|
248
|
+
while (next < unique.length) {
|
|
249
|
+
const name = unique[next++];
|
|
250
|
+
result[name] = await this.fetchOne(name, registry);
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
await Promise.all(Array.from({ length: Math.min(this.concurrency, unique.length) }, worker));
|
|
254
|
+
if (this.dirty) {
|
|
255
|
+
this.dirty = false;
|
|
256
|
+
this.persist();
|
|
257
|
+
}
|
|
258
|
+
return result;
|
|
259
|
+
}
|
|
260
|
+
}
|