@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
package/lib/check.js
ADDED
|
@@ -0,0 +1,1067 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Profile composition diagnostics — issue #98 (phase 1): the check-only
|
|
3
|
+
* "plugin loading layer and conflict view".
|
|
4
|
+
*
|
|
5
|
+
* Pure filesystem analysis of one dsh profile directory; no processes, no
|
|
6
|
+
* network, no writes. It answers, for the profile the market is serving:
|
|
7
|
+
*
|
|
8
|
+
* 1. What is the actual bundle stack (dsh.profile.bundles order) and where
|
|
9
|
+
* does each layer come from (official in-box bundle vs community, the
|
|
10
|
+
* dependency spec, the resolved directory)?
|
|
11
|
+
* 2. Which loader entry ids does the composed tree contain, and are any
|
|
12
|
+
* duplicated across layers (the "duplicate loader entry id" boot failure
|
|
13
|
+
* from #98)? Which rows does a later layer override?
|
|
14
|
+
* 3. Does any installed plugin pull a DSH host core package
|
|
15
|
+
* (@deepseek-ai/dsh, @deepseek-ai/dsh-tools, @deepseek-ai/cordis, …) in
|
|
16
|
+
* as an ordinary dependency — the dsh-excel-chat failure mode where the
|
|
17
|
+
* plugin's copy gets hoisted to the profile root and shadows the host's
|
|
18
|
+
* version (tool calls die, minimal preset fails to mount)?
|
|
19
|
+
* 4. Are there multiple versions of one core package in the lockfile, and
|
|
20
|
+
* do plugin peerDependencies ranges match the resolved core version?
|
|
21
|
+
* 5. Do effective user/home patch entries reference npm package roots that
|
|
22
|
+
* are installed in the profile-visible node_modules ancestry?
|
|
23
|
+
*
|
|
24
|
+
* The composition step mirrors @deepseek-ai/dsh-app-boot's applyEntryPatches
|
|
25
|
+
* (same js-yaml dialect incl. `!!js` scalars), so the rows reported here are
|
|
26
|
+
* what actually mounts at boot.
|
|
27
|
+
*/
|
|
28
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
29
|
+
import { createRequire, isBuiltin } from 'node:module';
|
|
30
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
31
|
+
import { JSON_SCHEMA, Type, load } from 'js-yaml';
|
|
32
|
+
import { findDshInstallDir } from './dsh-install.js';
|
|
33
|
+
import { resolveDshHome } from './home-paths.js';
|
|
34
|
+
import { INBOX_BUNDLES, readBundleRules, suggestOrder, validateOrder } from './order.js';
|
|
35
|
+
export { findDshInstallDir } from './dsh-install.js';
|
|
36
|
+
/** js-yaml dialect for `!!js` scalars — identical to dsh-app-boot's entryListSchema. */
|
|
37
|
+
const jsExpr = new Type('tag:yaml.org,2002:js', {
|
|
38
|
+
kind: 'scalar',
|
|
39
|
+
resolve: (data) => typeof data === 'string',
|
|
40
|
+
construct: (data) => ({ __jsExpr: String(data) }),
|
|
41
|
+
});
|
|
42
|
+
const entrySchema = JSON_SCHEMA.extend(jsExpr);
|
|
43
|
+
function isRecord(value) {
|
|
44
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
45
|
+
}
|
|
46
|
+
/** Parse entry-list source with the DSH dialect; null when it is not a list. */
|
|
47
|
+
export function parsePatchText(text) {
|
|
48
|
+
try {
|
|
49
|
+
const value = load(text, { schema: entrySchema });
|
|
50
|
+
return Array.isArray(value) ? value : null;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** Parse one entry-list patch file with the DSH dialect; null when unreadable. */
|
|
57
|
+
export function parsePatchFile(path) {
|
|
58
|
+
try {
|
|
59
|
+
return parsePatchText(readFileSync(path, 'utf8'));
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** Whether `name` goes through the Loader's bare-module resolver. */
|
|
66
|
+
function isBareLoaderSpecifier(name) {
|
|
67
|
+
return !name.startsWith('.')
|
|
68
|
+
&& !name.startsWith('#')
|
|
69
|
+
&& !isAbsolute(name)
|
|
70
|
+
&& !/^[a-z][a-z\d+.-]*:/i.test(name);
|
|
71
|
+
}
|
|
72
|
+
/** Npm package root owning one bare Loader specifier. */
|
|
73
|
+
function packageRoot(specifier) {
|
|
74
|
+
const parts = specifier.split('/');
|
|
75
|
+
const segments = specifier.startsWith('@') ? parts.slice(0, 2) : parts.slice(0, 1);
|
|
76
|
+
if (segments.length !== (specifier.startsWith('@') ? 2 : 1))
|
|
77
|
+
return null;
|
|
78
|
+
if (segments.some(segment => segment === '' || segment === '.' || segment === '..'
|
|
79
|
+
|| segment.includes('%') || segment.includes('\\')))
|
|
80
|
+
return null;
|
|
81
|
+
if (specifier.startsWith('@') && (segments[0]?.length ?? 0) <= 1)
|
|
82
|
+
return null;
|
|
83
|
+
return segments.join('/');
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Package-root presence from the Loader-visible profile ancestry only.
|
|
87
|
+
* An explicit walk avoids CommonJS global lookup directories that
|
|
88
|
+
* `createRequire(...).resolve.paths()` appends but Node ESM does not search.
|
|
89
|
+
*/
|
|
90
|
+
function profilePackageInstalled(profileDirectory, name) {
|
|
91
|
+
try {
|
|
92
|
+
const profile = JSON.parse(readFileSync(join(profileDirectory, 'package.json'), 'utf8'));
|
|
93
|
+
if (isRecord(profile) && profile.name === name
|
|
94
|
+
&& profile.exports !== undefined && profile.exports !== null)
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
catch { /* not a self-referencing profile package */ }
|
|
98
|
+
let directory = resolve(profileDirectory);
|
|
99
|
+
while (true) {
|
|
100
|
+
const packageDirectory = join(directory, 'node_modules', name);
|
|
101
|
+
// Node stops at the nearest matching package directory. A partial install
|
|
102
|
+
// there shadows any healthy parent copy and must not be accepted.
|
|
103
|
+
try {
|
|
104
|
+
if (statSync(packageDirectory).isDirectory()) {
|
|
105
|
+
return existsSync(join(packageDirectory, 'package.json'));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch { /* keep walking */ }
|
|
109
|
+
const parent = dirname(directory);
|
|
110
|
+
if (parent === directory)
|
|
111
|
+
return false;
|
|
112
|
+
directory = parent;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/** Every id in one patch row's insert list, recursively (group configs included). */
|
|
116
|
+
function collectInsertIds(rows) {
|
|
117
|
+
const ids = [];
|
|
118
|
+
const walk = (value) => {
|
|
119
|
+
if (!Array.isArray(value))
|
|
120
|
+
return;
|
|
121
|
+
for (const entry of value) {
|
|
122
|
+
if (!isRecord(entry) || typeof entry.id !== 'string')
|
|
123
|
+
continue;
|
|
124
|
+
ids.push(entry.id);
|
|
125
|
+
if (Array.isArray(entry.config))
|
|
126
|
+
walk(entry.config);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
for (const patch of rows) {
|
|
130
|
+
if (!isRecord(patch) || !Array.isArray(patch.insert))
|
|
131
|
+
continue;
|
|
132
|
+
walk(patch.insert);
|
|
133
|
+
}
|
|
134
|
+
return ids;
|
|
135
|
+
}
|
|
136
|
+
/** DSH host core packages: what the dsh installation ships under @deepseek-ai. */
|
|
137
|
+
export function corePackageNames(dshInstallDir) {
|
|
138
|
+
const names = new Set([
|
|
139
|
+
// Curated fallback seed (used when the install dir cannot be located).
|
|
140
|
+
'@deepseek-ai/dsh',
|
|
141
|
+
'@deepseek-ai/dsh-base',
|
|
142
|
+
'@deepseek-ai/dsh-web-app',
|
|
143
|
+
'@deepseek-ai/dsh-headless',
|
|
144
|
+
'@deepseek-ai/dsh-app-boot',
|
|
145
|
+
'@deepseek-ai/dsh-home-paths',
|
|
146
|
+
'@deepseek-ai/dsh-launch-environment',
|
|
147
|
+
'@deepseek-ai/dsh-cmdline',
|
|
148
|
+
'@deepseek-ai/dsh-tools',
|
|
149
|
+
'@deepseek-ai/dsh-llm',
|
|
150
|
+
'@deepseek-ai/dsh-system-prompt',
|
|
151
|
+
'@deepseek-ai/dsh-attachment',
|
|
152
|
+
'@deepseek-ai/dsh-agent',
|
|
153
|
+
'@deepseek-ai/dsh-agent-loop',
|
|
154
|
+
'@deepseek-ai/dsh-session',
|
|
155
|
+
'@deepseek-ai/dsh-subagent',
|
|
156
|
+
'@deepseek-ai/cordis',
|
|
157
|
+
'@deepseek-ai/cordis-plugin-loader',
|
|
158
|
+
'@deepseek-ai/cordis-plugin-include',
|
|
159
|
+
'@deepseek-ai/cordis-plugin-hmr',
|
|
160
|
+
'@deepseek-ai/cordis-plugin-timer',
|
|
161
|
+
'@deepseek-ai/cordis-plugin-group',
|
|
162
|
+
]);
|
|
163
|
+
if (dshInstallDir === null)
|
|
164
|
+
return names;
|
|
165
|
+
try {
|
|
166
|
+
// The install's own node_modules is the authoritative host-core inventory:
|
|
167
|
+
// everything under @deepseek-ai whose bare name starts with dsh or cordis,
|
|
168
|
+
// plus the app package itself.
|
|
169
|
+
for (const entry of readdirSync(join(dshInstallDir, 'node_modules', '@deepseek-ai'), { withFileTypes: true })) {
|
|
170
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink())
|
|
171
|
+
continue;
|
|
172
|
+
if (/^(?:dsh|cordis)/.test(entry.name))
|
|
173
|
+
names.add(`@deepseek-ai/${entry.name}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
catch { /* install node_modules unreadable — the curated seed stands */ }
|
|
177
|
+
try {
|
|
178
|
+
const manifest = JSON.parse(readFileSync(join(dshInstallDir, 'package.json'), 'utf8'));
|
|
179
|
+
if (typeof manifest.name === 'string')
|
|
180
|
+
names.add(manifest.name);
|
|
181
|
+
}
|
|
182
|
+
catch { /* not a package dir */ }
|
|
183
|
+
return names;
|
|
184
|
+
}
|
|
185
|
+
/** Version of `name` as physically resolved at `base`/node_modules, or null. */
|
|
186
|
+
function readNodeModulesVersion(base, name) {
|
|
187
|
+
try {
|
|
188
|
+
const manifest = JSON.parse(readFileSync(join(base, 'node_modules', name, 'package.json'), 'utf8'));
|
|
189
|
+
return typeof manifest.version === 'string' ? manifest.version : null;
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Resolve one package's directory the way the dsh boot does
|
|
197
|
+
* (dsh-app-boot's resolveBundleDir): probe Node's own node_modules search
|
|
198
|
+
* paths from the installation anchor first, then the profile directory.
|
|
199
|
+
* Node resolution walks upward, so this also finds pnpm's workspace-root
|
|
200
|
+
* hoisting (`<profiles>/node_modules/…` when the profile lives under
|
|
201
|
+
* `<profiles>/<name>`) and mirrors the Loader's package search roots.
|
|
202
|
+
*/
|
|
203
|
+
function resolvePackageDir(anchorPackageJson, name, ignoredPackageDirectory) {
|
|
204
|
+
let paths = [];
|
|
205
|
+
try {
|
|
206
|
+
paths = createRequire(anchorPackageJson).resolve.paths(name) ?? [];
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
const ignored = ignoredPackageDirectory === undefined
|
|
212
|
+
? null
|
|
213
|
+
: resolve(ignoredPackageDirectory);
|
|
214
|
+
for (const searchPath of paths) {
|
|
215
|
+
const candidate = join(searchPath, name);
|
|
216
|
+
if (ignored !== null) {
|
|
217
|
+
const resolvedCandidate = resolve(candidate);
|
|
218
|
+
const matchesIgnored = process.platform === 'win32'
|
|
219
|
+
? resolvedCandidate.toLowerCase() === ignored.toLowerCase()
|
|
220
|
+
: resolvedCandidate === ignored;
|
|
221
|
+
if (matchesIgnored)
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (existsSync(join(candidate, 'package.json')))
|
|
225
|
+
return candidate;
|
|
226
|
+
}
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Version of `name` visible to the profile's dependency tree: the profile's
|
|
231
|
+
* own node_modules first, then the workspace root (pnpm hoists shared deps
|
|
232
|
+
* there when the profile is a workspace member — the dsh layout keeps
|
|
233
|
+
* `<profiles>/node_modules` as the shared store for all profiles).
|
|
234
|
+
*/
|
|
235
|
+
function readProfileVisibleVersion(profileDirectory, name) {
|
|
236
|
+
const direct = readNodeModulesVersion(profileDirectory, name);
|
|
237
|
+
if (direct !== null)
|
|
238
|
+
return direct;
|
|
239
|
+
const workspaceRoot = dirname(profileDirectory);
|
|
240
|
+
if (workspaceRoot === profileDirectory)
|
|
241
|
+
return null;
|
|
242
|
+
return readNodeModulesVersion(workspaceRoot, name);
|
|
243
|
+
}
|
|
244
|
+
/** Top-level installed package names (incl. scoped), excluding pnpm internals. */
|
|
245
|
+
function installedPackageNames(profileDir) {
|
|
246
|
+
const names = [];
|
|
247
|
+
// Windows `link:` installs are junctions; Dirent.isDirectory() is false for
|
|
248
|
+
// them on some Node versions, so treat symlinks as packages too (B2).
|
|
249
|
+
const isPkgDir = (entry) => entry.isDirectory() || entry.isSymbolicLink();
|
|
250
|
+
let root;
|
|
251
|
+
try {
|
|
252
|
+
root = readdirSync(join(profileDir, 'node_modules'), { withFileTypes: true })
|
|
253
|
+
.filter(entry => isPkgDir(entry) && entry.name !== '.bin' && entry.name !== '.pnpm' && entry.name !== '.dsh-plugin-backups')
|
|
254
|
+
.map(entry => entry.name);
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
return names;
|
|
258
|
+
}
|
|
259
|
+
for (const name of root) {
|
|
260
|
+
if (!name.startsWith('@')) {
|
|
261
|
+
names.push(name);
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
try {
|
|
265
|
+
for (const scoped of readdirSync(join(profileDir, 'node_modules', name), { withFileTypes: true })) {
|
|
266
|
+
if (isPkgDir(scoped))
|
|
267
|
+
names.push(`${name}/${scoped.name}`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
catch { /* empty scope dir */ }
|
|
271
|
+
}
|
|
272
|
+
return names;
|
|
273
|
+
}
|
|
274
|
+
const SEMVER_RE = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
275
|
+
function parseSemver(value) {
|
|
276
|
+
const m = SEMVER_RE.exec(value.trim());
|
|
277
|
+
if (m === null)
|
|
278
|
+
return null;
|
|
279
|
+
return {
|
|
280
|
+
major: Number(m[1]),
|
|
281
|
+
minor: Number(m[2]),
|
|
282
|
+
patch: Number(m[3]),
|
|
283
|
+
pre: m[4] === undefined ? [] : m[4].split('.'),
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
function comparePre(a, b) {
|
|
287
|
+
const len = Math.max(a.length, b.length);
|
|
288
|
+
for (let i = 0; i < len; i += 1) {
|
|
289
|
+
const x = a[i];
|
|
290
|
+
const y = b[i];
|
|
291
|
+
if (x === undefined)
|
|
292
|
+
return y === undefined ? 0 : -1; // a has fewer parts → a < b
|
|
293
|
+
if (y === undefined)
|
|
294
|
+
return 1;
|
|
295
|
+
if (x === y)
|
|
296
|
+
continue;
|
|
297
|
+
const xn = /^\d+$/.test(x);
|
|
298
|
+
const yn = /^\d+$/.test(y);
|
|
299
|
+
if (xn && yn)
|
|
300
|
+
return Number(x) - Number(y) || 0;
|
|
301
|
+
if (xn)
|
|
302
|
+
return -1; // numeric identifiers sort before alphanumeric
|
|
303
|
+
if (yn)
|
|
304
|
+
return 1;
|
|
305
|
+
return x < y ? -1 : 1;
|
|
306
|
+
}
|
|
307
|
+
return 0;
|
|
308
|
+
}
|
|
309
|
+
/** Compare two semver strings: negative | zero | positive (prerelease < release of same base). */
|
|
310
|
+
export function compareSemver(a, b) {
|
|
311
|
+
const av = parseSemver(a);
|
|
312
|
+
const bv = parseSemver(b);
|
|
313
|
+
if (av === null || bv === null)
|
|
314
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
315
|
+
if (av.major !== bv.major)
|
|
316
|
+
return av.major - bv.major || 0;
|
|
317
|
+
if (av.minor !== bv.minor)
|
|
318
|
+
return av.minor - bv.minor || 0;
|
|
319
|
+
if (av.patch !== bv.patch)
|
|
320
|
+
return av.patch - bv.patch || 0;
|
|
321
|
+
if (av.pre.length === 0 && bv.pre.length === 0)
|
|
322
|
+
return 0;
|
|
323
|
+
if (av.pre.length === 0)
|
|
324
|
+
return 1; // release > prerelease
|
|
325
|
+
if (bv.pre.length === 0)
|
|
326
|
+
return -1;
|
|
327
|
+
return comparePre(av.pre, bv.pre);
|
|
328
|
+
}
|
|
329
|
+
function gte(a, b) {
|
|
330
|
+
const cmp = compareSemver(`${a.major}.${a.minor}.${a.patch}${a.pre.length > 0 ? `-${a.pre.join('.')}` : ''}`, `${b.major}.${b.minor}.${b.patch}${b.pre.length > 0 ? `-${b.pre.join('.')}` : ''}`);
|
|
331
|
+
return cmp >= 0;
|
|
332
|
+
}
|
|
333
|
+
/** String form of a parsed Semver (the boundary objects carry no prerelease). */
|
|
334
|
+
function semverStr(v) {
|
|
335
|
+
return `${v.major}.${v.minor}.${v.patch}${v.pre.length > 0 ? `-${v.pre.join('.')}` : ''}`;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Minimal range matcher for the peer-range check: `*`, exact, ^, ~, >=, >,
|
|
339
|
+
* <=, <, whitespace-separated pairs, and `||` alternatives. Anything else
|
|
340
|
+
* returns null (unknown — reported, not asserted).
|
|
341
|
+
*
|
|
342
|
+
* Prerelease handling follows npm's semver rule, evaluated at the comparator
|
|
343
|
+
* SET level (one `||` alternative is one set): a version carrying a
|
|
344
|
+
* prerelease tag only satisfies a set when at least one comparator in that
|
|
345
|
+
* set shares the version's [major, minor, patch] tuple AND carries a
|
|
346
|
+
* prerelease of its own; then every comparator is checked normally. So
|
|
347
|
+
* `^0.1.0` never matches `0.2.0-rc.1` (nor `0.1.0-rc.5`), while
|
|
348
|
+
* `>=1.2.3-rc.1 <2.0.0` does match `1.2.3-rc.2` (issue #98 analysis).
|
|
349
|
+
* Discovery can opt into npm's `includePrerelease` behaviour because every
|
|
350
|
+
* published DSH host line is itself prerelease; diagnostics retain the npm
|
|
351
|
+
* default unless a caller explicitly asks for that wider admission.
|
|
352
|
+
*/
|
|
353
|
+
export function satisfiesRange(version, range, options = {}) {
|
|
354
|
+
const v = parseSemver(version);
|
|
355
|
+
if (v === null)
|
|
356
|
+
return null;
|
|
357
|
+
const versionHasPre = v.pre.length > 0;
|
|
358
|
+
// Mirror pnpm's peer-dependency publish transform. Unlike ordinary
|
|
359
|
+
// dependency specs, a workspace token may sit inside a larger peer range.
|
|
360
|
+
// Operator-only (or bare) tokens receive the linked sibling's version;
|
|
361
|
+
// explicit versions only lose the protocol prefix. Unsupported alias/path
|
|
362
|
+
// forms become an unknown range below rather than a definite mismatch.
|
|
363
|
+
const workspaceSemver = /workspace:([\^~*]|>=|>|<=|<)?((\d+|[xX*])(\.(\d+|[xX*])){0,2})?/;
|
|
364
|
+
let normalizedRange = range;
|
|
365
|
+
if (range.includes('workspace:')) {
|
|
366
|
+
const match = workspaceSemver.exec(range);
|
|
367
|
+
if (match === null) {
|
|
368
|
+
normalizedRange = range.replace('workspace:', '');
|
|
369
|
+
}
|
|
370
|
+
else if (match[2] !== undefined) {
|
|
371
|
+
normalizedRange = range.replace('workspace:', '');
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
const operator = match[1] === '*' ? '' : (match[1] ?? '');
|
|
375
|
+
normalizedRange = range.replace(workspaceSemver, `${operator}${semverStr(v)}`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
const single = (part) => {
|
|
379
|
+
const p = part.trim();
|
|
380
|
+
if (p === '' || p === '*' || p === 'x' || p === 'X')
|
|
381
|
+
return true;
|
|
382
|
+
const m = /^(\^|~|>=|<=|>|<)?(.*)$/.exec(p);
|
|
383
|
+
const op = m?.[1] ?? '';
|
|
384
|
+
const target = (m?.[2] ?? '').trim();
|
|
385
|
+
const tv = parseSemver(target);
|
|
386
|
+
if (tv === null)
|
|
387
|
+
return null;
|
|
388
|
+
const major = tv.major;
|
|
389
|
+
const minor = tv.minor;
|
|
390
|
+
const patch = tv.patch;
|
|
391
|
+
switch (op) {
|
|
392
|
+
case '':
|
|
393
|
+
return compareSemver(version, target) === 0;
|
|
394
|
+
case '>=':
|
|
395
|
+
return gte(v, tv);
|
|
396
|
+
case '<=':
|
|
397
|
+
return gte(tv, v);
|
|
398
|
+
case '>':
|
|
399
|
+
return compareSemver(version, target) > 0;
|
|
400
|
+
case '<':
|
|
401
|
+
return compareSemver(version, target) < 0;
|
|
402
|
+
case '^': {
|
|
403
|
+
// npm caret semantics: >= given, strictly < the next breaking bump.
|
|
404
|
+
const upper = major > 0
|
|
405
|
+
? { major: major + 1, minor: 0, patch: 0, pre: [] }
|
|
406
|
+
: minor > 0
|
|
407
|
+
? { major: 0, minor: minor + 1, patch: 0, pre: [] }
|
|
408
|
+
: { major: 0, minor: 0, patch: patch + 1, pre: [] };
|
|
409
|
+
return gte(v, tv) && compareSemver(semverStr(upper), version) > 0;
|
|
410
|
+
}
|
|
411
|
+
case '~': {
|
|
412
|
+
// npm tilde semantics: >= given, strictly < the next minor.
|
|
413
|
+
const upper = { major, minor: minor + 1, patch: 0, pre: [] };
|
|
414
|
+
return gte(v, tv) && compareSemver(semverStr(upper), version) > 0;
|
|
415
|
+
}
|
|
416
|
+
default:
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
/** Parse one comparator part into op + target; null when unknown. */
|
|
421
|
+
const comparator = (part) => {
|
|
422
|
+
const p = part.trim();
|
|
423
|
+
if (p === '' || p === '*' || p === 'x' || p === 'X')
|
|
424
|
+
return { op: '', target: '' };
|
|
425
|
+
const m = /^(\^|~|>=|<=|>|<)?(.*)$/.exec(p);
|
|
426
|
+
if (m === null)
|
|
427
|
+
return null;
|
|
428
|
+
const target = (m[2] ?? '').trim();
|
|
429
|
+
// Enforce this function's documented unknown-range contract before the
|
|
430
|
+
// prerelease admission gate. Otherwise an unknown protocol such as
|
|
431
|
+
// `workspace:^` or `catalog:default` is misreported as a definite false
|
|
432
|
+
// whenever the resolved version happens to carry a prerelease tag.
|
|
433
|
+
if (parseSemver(target) === null)
|
|
434
|
+
return null;
|
|
435
|
+
return { op: m[1] ?? '', target };
|
|
436
|
+
};
|
|
437
|
+
/** Evaluate ONE comparator set (a `||` alternative) as a conjunction. */
|
|
438
|
+
const evaluateSet = (set) => {
|
|
439
|
+
const parts = set.trim().split(/\s+/).filter(part => part !== '');
|
|
440
|
+
if (parts.length === 0)
|
|
441
|
+
return true;
|
|
442
|
+
const parsed = parts.map(part => comparator(part));
|
|
443
|
+
if (parsed.some(part => part === null))
|
|
444
|
+
return null;
|
|
445
|
+
if (versionHasPre && options.includePrerelease !== true) {
|
|
446
|
+
// npm gate (set-level): the set admits prerelease versions only when a
|
|
447
|
+
// comparator pins the SAME base tuple with a prerelease of its own.
|
|
448
|
+
const admitted = parsed.some((part) => {
|
|
449
|
+
if (part?.target === '')
|
|
450
|
+
return false;
|
|
451
|
+
const tv = parseSemver(part?.target ?? '');
|
|
452
|
+
return tv !== null && tv.pre.length > 0
|
|
453
|
+
&& v.major === tv.major && v.minor === tv.minor && v.patch === tv.patch;
|
|
454
|
+
});
|
|
455
|
+
if (!admitted)
|
|
456
|
+
return false;
|
|
457
|
+
}
|
|
458
|
+
const results = parsed.map(part => single(part?.op !== undefined ? `${part.op}${part.target}` : ''));
|
|
459
|
+
if (results.some(r => r === null))
|
|
460
|
+
return null;
|
|
461
|
+
return results.every(r => r === true);
|
|
462
|
+
};
|
|
463
|
+
if (normalizedRange.includes('||')) {
|
|
464
|
+
const outcomes = normalizedRange.split('||').map(part => evaluateSet(part));
|
|
465
|
+
if (outcomes.some(out => out === true))
|
|
466
|
+
return true;
|
|
467
|
+
if (outcomes.some(out => out === null))
|
|
468
|
+
return null;
|
|
469
|
+
return false;
|
|
470
|
+
}
|
|
471
|
+
return evaluateSet(normalizedRange);
|
|
472
|
+
}
|
|
473
|
+
/** Flatten a tree of entries (group configs included) into row records. */
|
|
474
|
+
function flattenEntries(nodes) {
|
|
475
|
+
const rows = [];
|
|
476
|
+
const walk = (list, inheritedLayer) => {
|
|
477
|
+
for (const node of list) {
|
|
478
|
+
// Nested group configs come straight from the parsed patch and do not
|
|
479
|
+
// carry the synthetic layer metadata attached to their parent insert.
|
|
480
|
+
const layer = typeof node.layer === 'string' ? node.layer : inheritedLayer;
|
|
481
|
+
if (layer === undefined)
|
|
482
|
+
continue;
|
|
483
|
+
rows.push({ id: node.id, layer, kind: 'insert', name: node.name });
|
|
484
|
+
if (node.group === true && Array.isArray(node.config)) {
|
|
485
|
+
walk(node.config, layer);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
walk(nodes);
|
|
490
|
+
return rows;
|
|
491
|
+
}
|
|
492
|
+
/** Literal values use Loader Boolean semantics; `!!js` stays indeterminate. */
|
|
493
|
+
function disabledState(value) {
|
|
494
|
+
if (isRecord(value) && typeof value.__jsExpr === 'string')
|
|
495
|
+
return 'conditional';
|
|
496
|
+
return Boolean(value) ? 'disabled' : 'active';
|
|
497
|
+
}
|
|
498
|
+
function combineDisabled(parent, own) {
|
|
499
|
+
if (parent === 'disabled' || own === 'disabled')
|
|
500
|
+
return 'disabled';
|
|
501
|
+
if (parent === 'conditional' || own === 'conditional')
|
|
502
|
+
return 'conditional';
|
|
503
|
+
return 'active';
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Rows whose specifiers the Loader can attempt to import. Group rows are
|
|
507
|
+
* always imported even when disabled (their disabled state gates children),
|
|
508
|
+
* while expression-gated non-group rows are retained as conditional.
|
|
509
|
+
*/
|
|
510
|
+
function resolvableEntries(nodes) {
|
|
511
|
+
const rows = [];
|
|
512
|
+
const walk = (list, inheritedLayer, parentDisabled = 'active') => {
|
|
513
|
+
for (const node of list) {
|
|
514
|
+
const layer = typeof node.layer === 'string' ? node.layer : inheritedLayer;
|
|
515
|
+
if (layer === undefined)
|
|
516
|
+
continue;
|
|
517
|
+
const descendantsDisabled = combineDisabled(parentDisabled, disabledState(node.disabled));
|
|
518
|
+
const activation = node.group === true ? 'required' : descendantsDisabled;
|
|
519
|
+
if (activation !== 'disabled') {
|
|
520
|
+
rows.push({
|
|
521
|
+
id: node.id,
|
|
522
|
+
layer,
|
|
523
|
+
kind: 'insert',
|
|
524
|
+
name: node.name,
|
|
525
|
+
activation: activation === 'conditional' ? 'conditional' : 'required',
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
if (node.group === true && Array.isArray(node.config)) {
|
|
529
|
+
walk(node.config, layer, descendantsDisabled);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
walk(nodes);
|
|
534
|
+
return rows;
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Apply the layer stack over an empty root exactly like the dsh boot include.
|
|
538
|
+
* Exported so the trial-start validation (src/trial.ts) can replay the
|
|
539
|
+
* composition with a candidate bundle order BEFORE anything is written.
|
|
540
|
+
*/
|
|
541
|
+
export function composeLayers(layers) {
|
|
542
|
+
const tree = [];
|
|
543
|
+
const orphans = [];
|
|
544
|
+
const overrides = [];
|
|
545
|
+
/**
|
|
546
|
+
* The boot's entryMap, mirrored incrementally: the LAST row registered for
|
|
547
|
+
* an id (top-level or nested group member) is the patch target, and later
|
|
548
|
+
* inserts overwrite the map entry — exactly dsh-app-boot's applyEntryPatches
|
|
549
|
+
* buildMap. Keeping the map instead of re-walking the tree pins the
|
|
550
|
+
* duplicate-id resolution to the boot's behavior (issue #98 analysis:
|
|
551
|
+
* explicit composition boundaries).
|
|
552
|
+
*/
|
|
553
|
+
const entryMap = new Map();
|
|
554
|
+
const buildMap = (nodes) => {
|
|
555
|
+
for (const node of nodes) {
|
|
556
|
+
if (node.id !== '')
|
|
557
|
+
entryMap.set(node.id, node);
|
|
558
|
+
if (node.group === true && Array.isArray(node.config))
|
|
559
|
+
buildMap(node.config);
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
for (const layer of layers) {
|
|
563
|
+
for (const patch of layer.patches) {
|
|
564
|
+
if (!isRecord(patch))
|
|
565
|
+
continue;
|
|
566
|
+
const { id, insert, name, ...overridesOf } = patch;
|
|
567
|
+
// Boot boundary: `insert` and `id` are truthiness-checked, so a falsy
|
|
568
|
+
// `insert` (null/''/0) falls through to the patch path and an empty id
|
|
569
|
+
// makes an insert a plain top-level append (applyEntryPatches).
|
|
570
|
+
const hasId = typeof id === 'string' ? id !== '' : Boolean(id);
|
|
571
|
+
const lookupKey = hasId ? String(id) : '';
|
|
572
|
+
if (insert) {
|
|
573
|
+
if (!Array.isArray(insert)) {
|
|
574
|
+
orphans.push({ id: lookupKey === '' ? '(anonymous)' : lookupKey, layer: layer.label, reason: 'insert is not an array' });
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
const nodes = insert.filter(isRecord).map((entry) => {
|
|
578
|
+
if (typeof entry.id !== 'string')
|
|
579
|
+
return null;
|
|
580
|
+
return {
|
|
581
|
+
id: entry.id,
|
|
582
|
+
name: typeof entry.name === 'string' ? entry.name : undefined,
|
|
583
|
+
layer: layer.label,
|
|
584
|
+
group: entry.group === true,
|
|
585
|
+
config: Array.isArray(entry.config) ? entry.config : undefined,
|
|
586
|
+
disabled: entry.disabled,
|
|
587
|
+
};
|
|
588
|
+
}).filter((n) => n !== null);
|
|
589
|
+
if (hasId) {
|
|
590
|
+
const target = entryMap.get(lookupKey);
|
|
591
|
+
if (target === undefined) {
|
|
592
|
+
orphans.push({ id: lookupKey, layer: layer.label, reason: 'insert target not found' });
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
if (target.group !== true) {
|
|
596
|
+
orphans.push({ id: lookupKey, layer: layer.label, reason: 'insert target is not a group' });
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
599
|
+
// Boot boundary: a group with a non-array config is fixed up to an
|
|
600
|
+
// empty array before the append (applyEntryPatches does the same).
|
|
601
|
+
if (!Array.isArray(target.config))
|
|
602
|
+
target.config = [];
|
|
603
|
+
target.config = [...target.config, ...nodes];
|
|
604
|
+
}
|
|
605
|
+
else {
|
|
606
|
+
tree.push(...nodes);
|
|
607
|
+
}
|
|
608
|
+
buildMap(nodes);
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
if (!hasId) {
|
|
612
|
+
orphans.push({ id: '(anonymous)', layer: layer.label, reason: 'id required for non-insert patch' });
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
const target = entryMap.get(lookupKey);
|
|
616
|
+
if (target === undefined) {
|
|
617
|
+
orphans.push({ id: lookupKey, layer: layer.label, reason: 'patch target not found' });
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
// Boot boundary: the name guard is truthiness-based — an empty-string
|
|
621
|
+
// name on the patch row does not trigger the mismatch skip.
|
|
622
|
+
if (name && name !== target.name) {
|
|
623
|
+
orphans.push({ id: lookupKey, layer: layer.label, reason: `name mismatch (expected ${String(target.name)}, got ${String(name)})` });
|
|
624
|
+
continue;
|
|
625
|
+
}
|
|
626
|
+
const priorLayers = [];
|
|
627
|
+
for (const node of flattenEntries(tree)) {
|
|
628
|
+
if (node.id === lookupKey && !priorLayers.includes(node.layer))
|
|
629
|
+
priorLayers.push(node.layer);
|
|
630
|
+
}
|
|
631
|
+
if (priorLayers.some(prior => prior !== layer.label)) {
|
|
632
|
+
overrides.push({ id: lookupKey, layer: layer.label, overriddenLayers: priorLayers.filter(prior => prior !== layer.label) });
|
|
633
|
+
}
|
|
634
|
+
for (const [key, value] of Object.entries(overridesOf)) {
|
|
635
|
+
if (key === 'id')
|
|
636
|
+
continue;
|
|
637
|
+
target[key] = value;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
const rows = flattenEntries(tree);
|
|
642
|
+
const resolvableRows = resolvableEntries(tree);
|
|
643
|
+
const byId = new Map();
|
|
644
|
+
for (const row of rows) {
|
|
645
|
+
const layers = byId.get(row.id) ?? [];
|
|
646
|
+
if (!layers.includes(row.layer))
|
|
647
|
+
layers.push(row.layer);
|
|
648
|
+
byId.set(row.id, layers);
|
|
649
|
+
}
|
|
650
|
+
const duplicates = [];
|
|
651
|
+
const counts = new Map();
|
|
652
|
+
for (const row of rows)
|
|
653
|
+
counts.set(row.id, (counts.get(row.id) ?? 0) + 1);
|
|
654
|
+
for (const [id, count] of counts) {
|
|
655
|
+
if (count < 2)
|
|
656
|
+
continue;
|
|
657
|
+
duplicates.push({ id, layers: byId.get(id) ?? [], count });
|
|
658
|
+
}
|
|
659
|
+
duplicates.sort((a, b) => a.id.localeCompare(b.id));
|
|
660
|
+
return { rows, resolvableRows, duplicates, overrides, orphans };
|
|
661
|
+
}
|
|
662
|
+
/** Distinct versions of `@deepseek-ai/{dsh,cordis}*` packages in the lockfile. */
|
|
663
|
+
function lockfileCoreVersions(profileDir) {
|
|
664
|
+
const found = new Map();
|
|
665
|
+
let text;
|
|
666
|
+
try {
|
|
667
|
+
text = readFileSync(join(profileDir, 'pnpm-lock.yaml'), 'utf8');
|
|
668
|
+
}
|
|
669
|
+
catch {
|
|
670
|
+
return new Map();
|
|
671
|
+
}
|
|
672
|
+
for (const m of text.matchAll(/(@deepseek-ai\/(?:dsh|cordis)[^@\s'"]*?)@([0-9][^\s:'"()]*)/g)) {
|
|
673
|
+
const name = m[1] ?? '';
|
|
674
|
+
// pnpm v9 peer-resolution keys carry a suffix: `name@1.0.0(peer@x)`. The
|
|
675
|
+
// version capture stops at the `(` — and at `)` too, so the peer
|
|
676
|
+
// reference INSIDE the suffix (`name@1.0.0(@deepseek-ai/dsh-base@4.0.1)`)
|
|
677
|
+
// never yields a fake `4.0.1)` version that would invent a phantom
|
|
678
|
+
// multi-version report on a healthy profile (issue #98 analysis).
|
|
679
|
+
const version = m[2] ?? '';
|
|
680
|
+
// Registry resolutions only: skip link:/git forms and anything that is
|
|
681
|
+
// not a well-formed semver (parseSemver double-checks the capture).
|
|
682
|
+
if (parseSemver(version) === null)
|
|
683
|
+
continue;
|
|
684
|
+
const versions = found.get(name) ?? new Set();
|
|
685
|
+
versions.add(version);
|
|
686
|
+
found.set(name, versions);
|
|
687
|
+
}
|
|
688
|
+
const out = new Map();
|
|
689
|
+
for (const [name, versions] of found)
|
|
690
|
+
out.set(name, [...versions].sort(compareSemver));
|
|
691
|
+
return out;
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Build the bundle layer stack for a profile under a GIVEN bundle order —
|
|
695
|
+
* the manifest order for analyzeProfile, or a candidate order for trial
|
|
696
|
+
* validation (src/trial.ts). Bundle resolution mirrors the boot exactly:
|
|
697
|
+
* the dsh installation anchor first (in-box bundles always come from the
|
|
698
|
+
* running dsh, never a profile-local copy), then Node's module search from
|
|
699
|
+
* the profile directory (covers community bundles and pnpm workspace-root
|
|
700
|
+
* hoisting). A single code path keeps the check report and the trial
|
|
701
|
+
* validation from ever disagreeing about what a bundle is or where it lives.
|
|
702
|
+
*/
|
|
703
|
+
export function buildBundleLayers(profileDirectory, bundleNames, specs, dshInstallDir) {
|
|
704
|
+
const bundles = bundleNames.map((name) => {
|
|
705
|
+
// The real loader gives the DSH installation first refusal for in-box
|
|
706
|
+
// bundles. Desktop keeps that installation private from plugins, so a
|
|
707
|
+
// DIRECT profile-local copy with the same official name is only a stale
|
|
708
|
+
// shadow, never evidence for the layer the running host loaded (#371).
|
|
709
|
+
// Keep walking the profile anchor's parent search paths: Desktop heals an
|
|
710
|
+
// authoritative host fallback at <profiles>/node_modules.
|
|
711
|
+
const ignoredProfilePackage = dshInstallDir === null && INBOX_BUNDLES.has(name)
|
|
712
|
+
? join(profileDirectory, 'node_modules', name)
|
|
713
|
+
: undefined;
|
|
714
|
+
const anchors = [
|
|
715
|
+
{ anchor: dshInstallDir !== null ? join(dshInstallDir, 'package.json') : null },
|
|
716
|
+
{
|
|
717
|
+
anchor: join(profileDirectory, 'package.json'),
|
|
718
|
+
ignoredPackageDirectory: ignoredProfilePackage,
|
|
719
|
+
},
|
|
720
|
+
];
|
|
721
|
+
let directory = null;
|
|
722
|
+
for (const { anchor, ignoredPackageDirectory } of anchors) {
|
|
723
|
+
if (anchor === null)
|
|
724
|
+
continue;
|
|
725
|
+
directory = resolvePackageDir(anchor, name, ignoredPackageDirectory);
|
|
726
|
+
if (directory !== null)
|
|
727
|
+
break;
|
|
728
|
+
}
|
|
729
|
+
const layer = {
|
|
730
|
+
name,
|
|
731
|
+
source: specs[name] ?? '(not a direct dependency)',
|
|
732
|
+
kind: INBOX_BUNDLES.has(name) ? 'official' : 'community',
|
|
733
|
+
directory,
|
|
734
|
+
patchPath: null,
|
|
735
|
+
error: null,
|
|
736
|
+
entries: [],
|
|
737
|
+
parseError: null,
|
|
738
|
+
};
|
|
739
|
+
if (directory === null) {
|
|
740
|
+
// An in-box bundle is supplied by the dsh INSTALLATION, not by the
|
|
741
|
+
// profile — that is what makes it in-box. So failing to find one says
|
|
742
|
+
// we could not locate the installation, not that the profile is
|
|
743
|
+
// broken: unusual or older hosts can still hide the in-box installation
|
|
744
|
+
// from both the CLI-entry and packaged-Desktop discovery anchors.
|
|
745
|
+
//
|
|
746
|
+
// Calling that "will fail to boot" turned a working composition into a
|
|
747
|
+
// fatal verdict and rolled back a good update (#369) — while `dsh
|
|
748
|
+
// --dump-config` on the same profile exited 0. Unknown has to read as
|
|
749
|
+
// unknown; the profile's own bundles are still judged normally.
|
|
750
|
+
if (INBOX_BUNDLES.has(name)) {
|
|
751
|
+
layer.error = null;
|
|
752
|
+
layer.unresolvedInbox = true;
|
|
753
|
+
return layer;
|
|
754
|
+
}
|
|
755
|
+
layer.error = 'bundle package is not installed — the profile will fail to boot';
|
|
756
|
+
return layer;
|
|
757
|
+
}
|
|
758
|
+
let bundleManifest;
|
|
759
|
+
try {
|
|
760
|
+
bundleManifest = JSON.parse(readFileSync(join(directory, 'package.json'), 'utf8'));
|
|
761
|
+
}
|
|
762
|
+
catch {
|
|
763
|
+
layer.error = 'bundle package.json is unreadable';
|
|
764
|
+
return layer;
|
|
765
|
+
}
|
|
766
|
+
const declared = bundleManifest.dsh?.bundle?.patch;
|
|
767
|
+
if (typeof declared !== 'string') {
|
|
768
|
+
layer.error = 'bundle declares no dsh.bundle.patch — the profile will fail to boot';
|
|
769
|
+
return layer;
|
|
770
|
+
}
|
|
771
|
+
const patchPath = join(directory, declared);
|
|
772
|
+
if (!existsSync(patchPath)) {
|
|
773
|
+
layer.error = `declared patch ${declared} is missing — the profile will fail to boot`;
|
|
774
|
+
return layer;
|
|
775
|
+
}
|
|
776
|
+
layer.patchPath = patchPath;
|
|
777
|
+
const patches = parsePatchFile(patchPath);
|
|
778
|
+
if (patches === null) {
|
|
779
|
+
layer.parseError = 'patch file is not a valid entry list';
|
|
780
|
+
return layer;
|
|
781
|
+
}
|
|
782
|
+
layer.entries = collectInsertIds(patches);
|
|
783
|
+
const order = bundleManifest.dsh?.bundle?.order;
|
|
784
|
+
if (order !== null && typeof order === 'object' && !Array.isArray(order)) {
|
|
785
|
+
const listOf = (value) => Array.isArray(value)
|
|
786
|
+
? value.filter((item) => typeof item === 'string')
|
|
787
|
+
: undefined;
|
|
788
|
+
const after = listOf(order.after);
|
|
789
|
+
const before = listOf(order.before);
|
|
790
|
+
if (after !== undefined || before !== undefined) {
|
|
791
|
+
layer.order = { ...(before !== undefined ? { before } : {}), ...(after !== undefined ? { after } : {}) };
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
return layer;
|
|
795
|
+
});
|
|
796
|
+
const layers = bundles.map((bundle) => ({
|
|
797
|
+
label: bundle.name,
|
|
798
|
+
kind: 'bundle',
|
|
799
|
+
patches: bundle.patchPath !== null && bundle.parseError === null ? parsePatchFile(bundle.patchPath) ?? [] : [],
|
|
800
|
+
parseError: bundle.parseError,
|
|
801
|
+
}));
|
|
802
|
+
return { bundles, layers };
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* Analyze one profile directory (issue #98, phase 1). Pure function of the
|
|
806
|
+
* directory contents — safe to call on every market open.
|
|
807
|
+
*/
|
|
808
|
+
export function analyzeProfile(profileDirectory, options = {}) {
|
|
809
|
+
const dshInstall = options.dshInstallDir ?? findDshInstallDir();
|
|
810
|
+
const home = resolveDshHome(options.homeDir);
|
|
811
|
+
const core = corePackageNames(dshInstall);
|
|
812
|
+
// --- 1. bundle stack ---
|
|
813
|
+
const manifest = (() => {
|
|
814
|
+
try {
|
|
815
|
+
return JSON.parse(readFileSync(join(profileDirectory, 'package.json'), 'utf8'));
|
|
816
|
+
}
|
|
817
|
+
catch {
|
|
818
|
+
return null;
|
|
819
|
+
}
|
|
820
|
+
})();
|
|
821
|
+
const bundleNames = Array.isArray(manifest?.dsh?.profile?.bundles)
|
|
822
|
+
? manifest.dsh.profile.bundles.filter((name) => typeof name === 'string')
|
|
823
|
+
: [];
|
|
824
|
+
const specs = manifest?.dependencies ?? {};
|
|
825
|
+
const built = buildBundleLayers(profileDirectory, bundleNames, specs, dshInstall);
|
|
826
|
+
const bundles = built.bundles;
|
|
827
|
+
const bundleLayers = built.layers;
|
|
828
|
+
// --- 2. composed loader rows / duplicates / overrides / orphans ---
|
|
829
|
+
const layers = [...bundleLayers];
|
|
830
|
+
const userPatchPath = join(profileDirectory, 'cordis.patch.yml');
|
|
831
|
+
if (existsSync(userPatchPath)) {
|
|
832
|
+
const patches = parsePatchFile(userPatchPath);
|
|
833
|
+
layers.push({ label: 'user-patch', kind: 'user', patches: patches ?? [], parseError: patches === null ? 'patch file is not a valid entry list' : null });
|
|
834
|
+
}
|
|
835
|
+
const homePatchPath = join(home, 'cordis.patch.yml');
|
|
836
|
+
if (existsSync(homePatchPath)) {
|
|
837
|
+
const patches = parsePatchFile(homePatchPath);
|
|
838
|
+
layers.push({ label: 'home-patch', kind: 'home', patches: patches ?? [], parseError: patches === null ? 'patch file is not a valid entry list' : null });
|
|
839
|
+
}
|
|
840
|
+
const composed = composeLayers(layers);
|
|
841
|
+
// --- 3. peer dependency mismatches (every declared peer, core or not) ---
|
|
842
|
+
// NOTE (issue #98 division of labor): the core-package-as-ordinary-
|
|
843
|
+
// dependency check (coreDeps / shadowing) belongs to @yzke's manifest PR;
|
|
844
|
+
// PR-A keeps only the peer-resolution check here.
|
|
845
|
+
const peerMismatches = [];
|
|
846
|
+
const seenDeps = new Set();
|
|
847
|
+
for (const plugin of installedPackageNames(profileDirectory)) {
|
|
848
|
+
let pkg;
|
|
849
|
+
try {
|
|
850
|
+
pkg = JSON.parse(readFileSync(join(profileDirectory, 'node_modules', plugin, 'package.json'), 'utf8'));
|
|
851
|
+
}
|
|
852
|
+
catch {
|
|
853
|
+
continue;
|
|
854
|
+
}
|
|
855
|
+
const pluginDir = join(profileDirectory, 'node_modules', plugin);
|
|
856
|
+
const map = pkg.peerDependencies;
|
|
857
|
+
if (map === null || typeof map !== 'object')
|
|
858
|
+
continue;
|
|
859
|
+
for (const [name, spec] of Object.entries(map)) {
|
|
860
|
+
if (typeof spec !== 'string')
|
|
861
|
+
continue;
|
|
862
|
+
const key = `${plugin}\u0000${name}\u0000peer`;
|
|
863
|
+
if (seenDeps.has(key))
|
|
864
|
+
continue;
|
|
865
|
+
seenDeps.add(key);
|
|
866
|
+
const hoisted = readProfileVisibleVersion(profileDirectory, name);
|
|
867
|
+
const nested = readNodeModulesVersion(pluginDir, name);
|
|
868
|
+
const host = dshInstall !== null ? readNodeModulesVersion(dshInstall, name) : null;
|
|
869
|
+
// Node resolves a plugin's peer from its OWN node_modules first
|
|
870
|
+
// (nested), then the profile tree (hoisted), then the host install.
|
|
871
|
+
const resolved = nested ?? hoisted ?? host;
|
|
872
|
+
// Peer checks cover EVERY declared peer, not just host core packages:
|
|
873
|
+
// plugin-to-plugin peer mismatches break runtime registration just as
|
|
874
|
+
// hard (issue #98 optimization round).
|
|
875
|
+
const satisfied = resolved !== null ? satisfiesRange(resolved, spec) : null;
|
|
876
|
+
const optional = pkg.peerDependenciesMeta?.[name]?.optional === true;
|
|
877
|
+
peerMismatches.push({
|
|
878
|
+
plugin, name, range: spec, resolved,
|
|
879
|
+
satisfied: satisfied === null ? null : satisfied,
|
|
880
|
+
...(optional ? { optional: true } : {}),
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
// --- 4. multi-version core packages from the lockfile ---
|
|
885
|
+
const multiVersion = [];
|
|
886
|
+
for (const [name, versions] of lockfileCoreVersions(profileDirectory)) {
|
|
887
|
+
if (versions.length < 2)
|
|
888
|
+
continue;
|
|
889
|
+
multiVersion.push({ name, versions, hoisted: readProfileVisibleVersion(profileDirectory, name) });
|
|
890
|
+
}
|
|
891
|
+
multiVersion.sort((a, b) => a.name.localeCompare(b.name));
|
|
892
|
+
// --- summary ---
|
|
893
|
+
const errors = [];
|
|
894
|
+
const warnings = [];
|
|
895
|
+
for (const bundle of bundles) {
|
|
896
|
+
if (bundle.error !== null)
|
|
897
|
+
errors.push(`bundle ${bundle.name}: ${bundle.error}`);
|
|
898
|
+
if (bundle.parseError !== null)
|
|
899
|
+
errors.push(`bundle ${bundle.name}: ${bundle.parseError}`);
|
|
900
|
+
}
|
|
901
|
+
for (const layer of layers) {
|
|
902
|
+
if (layer.parseError !== null && layer.kind !== 'bundle')
|
|
903
|
+
errors.push(`${layer.label}: ${layer.parseError}`);
|
|
904
|
+
}
|
|
905
|
+
// User and home patches can insert packages independently of a bundle.
|
|
906
|
+
// Only check rows that survived composition: an insert targeting a missing
|
|
907
|
+
// group is skipped by the boot and therefore cannot cause module loading to
|
|
908
|
+
// fail. Normalize package subpaths to their npm root and check the profile's
|
|
909
|
+
// node_modules ancestry; exact exports/subpath validation and relative,
|
|
910
|
+
// absolute, URL, builtin, package-import (#), or cordis: specifiers are
|
|
911
|
+
// outside this check.
|
|
912
|
+
const userLayerLabels = new Set(layers
|
|
913
|
+
.filter(layer => layer.kind === 'user' || layer.kind === 'home')
|
|
914
|
+
.map(layer => layer.label));
|
|
915
|
+
const candidates = new Map();
|
|
916
|
+
for (const row of composed.resolvableRows) {
|
|
917
|
+
if (row.kind !== 'insert' || !userLayerLabels.has(row.layer))
|
|
918
|
+
continue;
|
|
919
|
+
if (row.name === undefined || row.name === '') {
|
|
920
|
+
const message = `${row.layer}: loader entry ${JSON.stringify(row.id)} has no module name`;
|
|
921
|
+
if (row.activation === 'required')
|
|
922
|
+
errors.push(`${message} — the profile will fail to boot`);
|
|
923
|
+
else
|
|
924
|
+
warnings.push(`${message} — boot will fail if its disabled expression enables the entry`);
|
|
925
|
+
continue;
|
|
926
|
+
}
|
|
927
|
+
if (!isBareLoaderSpecifier(row.name))
|
|
928
|
+
continue;
|
|
929
|
+
if (isBuiltin(row.name))
|
|
930
|
+
continue;
|
|
931
|
+
const packageName = packageRoot(row.name);
|
|
932
|
+
if (packageName === null) {
|
|
933
|
+
const message = `${row.layer}: loader specifier ${JSON.stringify(row.name)} is not a valid bare package name`;
|
|
934
|
+
if (row.activation === 'required')
|
|
935
|
+
errors.push(`${message} — the profile will fail to boot`);
|
|
936
|
+
else
|
|
937
|
+
warnings.push(`${message} — boot will fail if its disabled expression enables the entry`);
|
|
938
|
+
continue;
|
|
939
|
+
}
|
|
940
|
+
const key = `${row.layer}\u0000${packageName}`;
|
|
941
|
+
const previous = candidates.get(key);
|
|
942
|
+
if (previous === undefined || previous.row.activation === 'conditional' && row.activation === 'required') {
|
|
943
|
+
candidates.set(key, { row, packageName });
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
for (const { row, packageName } of candidates.values()) {
|
|
947
|
+
if (profilePackageInstalled(profileDirectory, packageName))
|
|
948
|
+
continue;
|
|
949
|
+
const message = `${row.layer}: loader package ${packageName} is not installed in the profile`;
|
|
950
|
+
if (row.activation === 'required')
|
|
951
|
+
errors.push(`${message} — the profile will fail to boot`);
|
|
952
|
+
else
|
|
953
|
+
warnings.push(`${message} — boot will fail if its disabled expression enables the entry`);
|
|
954
|
+
}
|
|
955
|
+
for (const dup of composed.duplicates) {
|
|
956
|
+
errors.push(`duplicate loader entry id ${JSON.stringify(dup.id)} (${dup.count} rows: ${dup.layers.join(', ')})`);
|
|
957
|
+
}
|
|
958
|
+
for (const orphan of composed.orphans) {
|
|
959
|
+
warnings.push(`${orphan.layer}: ${orphan.id} — ${orphan.reason}`);
|
|
960
|
+
}
|
|
961
|
+
for (const mismatch of peerMismatches) {
|
|
962
|
+
// Only CONFIRMED incompatibilities warn. Un-evaluable peers (sat=null —
|
|
963
|
+
// the peer is supplied by the host, an optional accelerator, or simply
|
|
964
|
+
// absent) stay in the peerMismatches list for the UI but are not noise
|
|
965
|
+
// in the summary (issue #98 optimization round).
|
|
966
|
+
// ...and an OPTIONAL peer is not a confirmed incompatibility either.
|
|
967
|
+
// classifyPeer already downgrades those to non-risk (compatibility.ts),
|
|
968
|
+
// and the summary disagreeing with it meant a plugin that declares "I
|
|
969
|
+
// work without this" still produced a scary warning line — including
|
|
970
|
+
// for the market's own optional peer, on every profile that installs us
|
|
971
|
+
// (#275 by @tjxjxjx).
|
|
972
|
+
if (mismatch.satisfied === false && mismatch.optional !== true) {
|
|
973
|
+
warnings.push(`${mismatch.plugin} peer range ${mismatch.name}@${mismatch.range} does not match resolved ${String(mismatch.resolved)}`);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
for (const mv of multiVersion) {
|
|
977
|
+
const line = `${mv.name}: ${mv.versions.join(' / ')}${mv.hoisted !== null ? ` (hoisted ${mv.hoisted})` : ''}`;
|
|
978
|
+
if (core.has(mv.name))
|
|
979
|
+
errors.push(`multiple versions of core package — ${line}`);
|
|
980
|
+
else
|
|
981
|
+
warnings.push(`multiple versions of ${line}`);
|
|
982
|
+
}
|
|
983
|
+
// Current-order before/after rule conflicts (issue #98 phase 2): these are
|
|
984
|
+
// informative for the ordering UI; the author-declared rule breaking the
|
|
985
|
+
// CURRENT stack is worth a warning but not a boot failure. Conflicts are
|
|
986
|
+
// exposed both at the report top level and per bundle (order.conflicts) so
|
|
987
|
+
// the ordering panel can render them next to the bundle rows.
|
|
988
|
+
const orderConflicts = validateOrder(bundleNames, readBundleRules(profileDirectory));
|
|
989
|
+
for (const conflict of orderConflicts) {
|
|
990
|
+
warnings.push(`${conflict.name}: ${conflict.reason}`);
|
|
991
|
+
}
|
|
992
|
+
for (const bundle of bundles) {
|
|
993
|
+
const own = orderConflicts.filter(conflict => conflict.name === bundle.name);
|
|
994
|
+
if (own.length > 0)
|
|
995
|
+
bundle.order = { ...bundle.order, conflicts: own };
|
|
996
|
+
}
|
|
997
|
+
// LOOT-style auto-fix: suggest a minimal-change order satisfying every
|
|
998
|
+
// declared before/after rule. No rules → no suggestion (nothing to fix);
|
|
999
|
+
// with rules the suggestion keeps unconstrained bundles in their current
|
|
1000
|
+
// relative order (issue #125 review — never silently rewrites a hand-picked
|
|
1001
|
+
// order into an arbitrary canonical one).
|
|
1002
|
+
const suggestedOrder = suggestOrder(bundleNames, readBundleRules(profileDirectory));
|
|
1003
|
+
if (suggestedOrder === null) {
|
|
1004
|
+
// No declared rules — nothing to suggest and nothing to warn about.
|
|
1005
|
+
}
|
|
1006
|
+
else if (!suggestedOrder.ok) {
|
|
1007
|
+
warnings.push(`ordering constraints contain a cycle: ${suggestedOrder.cycle.join(' -> ')} — no compliant order exists / 排序约束存在循环依赖,无法得出合规顺序`);
|
|
1008
|
+
}
|
|
1009
|
+
else {
|
|
1010
|
+
// Only warn when the CURRENT order actually breaks a declared rule. A
|
|
1011
|
+
// hand-picked order that satisfies every rule but merely differs from the
|
|
1012
|
+
// suggestion is valid — flagging it would be a false alert on a healthy
|
|
1013
|
+
// profile (issue #98 analysis).
|
|
1014
|
+
if (orderConflicts.length > 0) {
|
|
1015
|
+
warnings.push('current bundle order violates declared rules — a better order is suggested / 当前 bundle 顺序违反声明规则,已给出更优顺序');
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
// Duplicate loader NAMES: the Loader registers plugins by name, so two rows
|
|
1019
|
+
// with the same name in DIFFERENT layers shadow each other at runtime (the
|
|
1020
|
+
// later layer wins). Rows sharing a name within ONE layer are routine — a
|
|
1021
|
+
// single bundle may define several entries under the same name — and are
|
|
1022
|
+
// skipped entirely: a same-layer "duplicate name" is a false positive, not
|
|
1023
|
+
// a conflict. The hard conflict is duplicate loader entry ids
|
|
1024
|
+
// (report.duplicates), which fail the boot outright and are unchanged.
|
|
1025
|
+
const nameCounts = new Map();
|
|
1026
|
+
for (const row of composed.rows) {
|
|
1027
|
+
if (row.name === undefined)
|
|
1028
|
+
continue;
|
|
1029
|
+
const layers = nameCounts.get(row.name) ?? [];
|
|
1030
|
+
if (!layers.includes(row.layer))
|
|
1031
|
+
layers.push(row.layer);
|
|
1032
|
+
nameCounts.set(row.name, layers);
|
|
1033
|
+
}
|
|
1034
|
+
const duplicateNames = [];
|
|
1035
|
+
for (const [name, layers] of nameCounts) {
|
|
1036
|
+
// Only cross-layer collisions are real shadowing candidates: all rows
|
|
1037
|
+
// with this name living in one layer is a normal multi-entry bundle.
|
|
1038
|
+
if (layers.length < 2)
|
|
1039
|
+
continue;
|
|
1040
|
+
const count = composed.rows.filter(row => row.name === name).length;
|
|
1041
|
+
duplicateNames.push({ name, layers, count });
|
|
1042
|
+
// Deliberately NOT pushed into summary.warnings: the report carries the
|
|
1043
|
+
// shadowing rows structurally (duplicateNames, rendered by the
|
|
1044
|
+
// diagnostics panel), and the cost of a false positive — a healthy
|
|
1045
|
+
// profile flagged with a warning — outweighs a false negative here.
|
|
1046
|
+
}
|
|
1047
|
+
duplicateNames.sort((a, b) => a.name.localeCompare(b.name));
|
|
1048
|
+
return {
|
|
1049
|
+
profile: profileDirectory,
|
|
1050
|
+
scannedAt: Date.now(),
|
|
1051
|
+
bundles,
|
|
1052
|
+
rows: composed.rows,
|
|
1053
|
+
duplicates: composed.duplicates,
|
|
1054
|
+
duplicateNames,
|
|
1055
|
+
overrides: composed.overrides,
|
|
1056
|
+
orphans: composed.orphans,
|
|
1057
|
+
peerMismatches,
|
|
1058
|
+
multiVersion,
|
|
1059
|
+
orderConflicts,
|
|
1060
|
+
suggestedOrder,
|
|
1061
|
+
summary: {
|
|
1062
|
+
ok: errors.length === 0,
|
|
1063
|
+
errors,
|
|
1064
|
+
warnings,
|
|
1065
|
+
},
|
|
1066
|
+
};
|
|
1067
|
+
}
|