@sparkelf/dsh-plus 0.2.4-alpha.47 → 0.2.5-alpha.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/bin.js +780 -326
- package/lib/types/bin.js +1 -1
- package/lib/types/capabilities.d.ts +139 -0
- package/lib/types/capabilities.js +688 -0
- package/lib/types/cli.d.ts +17 -0
- package/lib/types/cli.js +913 -0
- package/lib/types/manifest.d.ts +137 -0
- package/lib/types/manifest.js +398 -0
- package/lib/types/profile.d.ts +218 -0
- package/lib/types/profile.js +670 -0
- package/lib/types/server.d.ts +70 -0
- package/lib/types/server.js +158 -0
- package/lib/types/standalone-cli.js +274 -93
- package/lib/types/standalone-profile.d.ts +54 -17
- package/lib/types/standalone-profile.js +273 -247
- package/package.json +169 -124
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derive the profile's override table from the distribution's own facts.
|
|
3
|
+
*
|
|
4
|
+
* The distribution records one truth per fact: the patch set names the official packages it
|
|
5
|
+
* patches, the runtime names the version its republished builds carry, and an edition names the
|
|
6
|
+
* capabilities a deployment omits. The override table npm and pnpm read is a projection of those
|
|
7
|
+
* three, never a fourth list: an entry that cannot be derived fails the build instead of being
|
|
8
|
+
* carried forward, which is what kept a stale `@deepseek-ai/dsh-client-ui-slots` pin alive long
|
|
9
|
+
* after the patch behind it had gone.
|
|
10
|
+
*
|
|
11
|
+
* @module manifest
|
|
12
|
+
*/
|
|
13
|
+
/** One entry of the distribution's patch set, as far as override derivation reads it. */
|
|
14
|
+
export interface PatchEntryFacts {
|
|
15
|
+
readonly id: string;
|
|
16
|
+
readonly target: {
|
|
17
|
+
readonly kind: string;
|
|
18
|
+
readonly packages?: readonly string[];
|
|
19
|
+
readonly republish?: boolean;
|
|
20
|
+
readonly reason?: string;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/** Every fact the profile projection needs, validated at read time. */
|
|
24
|
+
export interface DistributionFacts {
|
|
25
|
+
readonly name: string;
|
|
26
|
+
readonly version: string;
|
|
27
|
+
/** Exact runtime the republished builds were cut against. */
|
|
28
|
+
readonly runtimeVersion: string;
|
|
29
|
+
/** Exact spec of the placeholder an omitted capability is substituted with. */
|
|
30
|
+
readonly omissionSpec: string;
|
|
31
|
+
readonly bundles: readonly string[];
|
|
32
|
+
readonly dependencies: Readonly<Record<string, string>>;
|
|
33
|
+
readonly allowBuilds: Readonly<Record<string, boolean>>;
|
|
34
|
+
/** Official package name to republished build spec. */
|
|
35
|
+
readonly redirects: Readonly<Record<string, string>>;
|
|
36
|
+
/** Patch entries whose payload reaches the registry through a redirect. */
|
|
37
|
+
readonly redirectIds: Readonly<Record<string, string>>;
|
|
38
|
+
/** Product editions: the only axis that selects a package and bundle set. */
|
|
39
|
+
readonly editions: Readonly<Record<string, ProfileEdition>>;
|
|
40
|
+
}
|
|
41
|
+
/** One product edition of the distribution. */
|
|
42
|
+
export interface ProfileEdition {
|
|
43
|
+
readonly label: string;
|
|
44
|
+
/** Profile name the launcher materializes for this edition. */
|
|
45
|
+
readonly profile: string;
|
|
46
|
+
/** Bundle rows the edition mounts. */
|
|
47
|
+
readonly bundles: readonly string[];
|
|
48
|
+
/** Package names the edition installs; versions come from `profile.dependencies`. */
|
|
49
|
+
readonly packages: readonly string[];
|
|
50
|
+
/** Capabilities the edition must not run, substituted with the omission placeholder. */
|
|
51
|
+
readonly omit: readonly string[];
|
|
52
|
+
/**
|
|
53
|
+
* Packages the edition does not install or mount at all.
|
|
54
|
+
*
|
|
55
|
+
* Unlike `omit`, nothing is substituted: the capability is absent rather than replaced. A
|
|
56
|
+
* deployment uses it to drop an optional capability the image supplies another way.
|
|
57
|
+
*/
|
|
58
|
+
readonly excludePackages: readonly string[];
|
|
59
|
+
}
|
|
60
|
+
/** A profile projected for one edition. */
|
|
61
|
+
export interface ProfileProjection {
|
|
62
|
+
readonly profileName: string;
|
|
63
|
+
readonly dependencies: Readonly<Record<string, string>>;
|
|
64
|
+
readonly bundles: readonly string[];
|
|
65
|
+
readonly overrides: Readonly<Record<string, string>>;
|
|
66
|
+
readonly allowBuilds: Readonly<Record<string, boolean>>;
|
|
67
|
+
readonly omittedPackages: Readonly<Record<string, string>>;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The republished scope name for one official package.
|
|
71
|
+
*
|
|
72
|
+
* @param official - an `@deepseek-ai/dsh-*` name.
|
|
73
|
+
* @returns the `@sparkelf/dsh-*` name the distribution publishes it as.
|
|
74
|
+
*/
|
|
75
|
+
export declare function republishedName(official: string): string;
|
|
76
|
+
/**
|
|
77
|
+
* Derive the redirect table from the patch set.
|
|
78
|
+
*
|
|
79
|
+
* Every `dsh-source` entry republishes the packages its payload modifies, because a registry
|
|
80
|
+
* installation has no source tree to patch. An entry that changes only types sets
|
|
81
|
+
* `target.republish: false` and must say why; anything else would look like a redirect that
|
|
82
|
+
* silently never happens.
|
|
83
|
+
*
|
|
84
|
+
* @param entries - the distribution's patch entries.
|
|
85
|
+
* @param runtimeVersion - exact version the republished builds carry.
|
|
86
|
+
* @returns official name to `npm:` alias spec.
|
|
87
|
+
*/
|
|
88
|
+
export declare function deriveRedirects(entries: readonly PatchEntryFacts[], runtimeVersion: string): {
|
|
89
|
+
redirects: Record<string, string>;
|
|
90
|
+
ids: Record<string, string>;
|
|
91
|
+
};
|
|
92
|
+
/** Read and validate the facts one installed distribution declares. */
|
|
93
|
+
export declare function readDistributionFacts(directory: string): DistributionFacts;
|
|
94
|
+
/**
|
|
95
|
+
* Project an edition into the profile npm and pnpm install.
|
|
96
|
+
*
|
|
97
|
+
* The distribution's reviewed order is the base; the edition names the bundle order and package
|
|
98
|
+
* set it installs and the capabilities it must not run. Versions always come from
|
|
99
|
+
* `profile.dependencies`, so an edition cannot pin a version the distribution did not review.
|
|
100
|
+
* No edition means the full distribution, which is what a launcher start without a selection
|
|
101
|
+
* materializes.
|
|
102
|
+
*
|
|
103
|
+
* @param facts - validated distribution facts.
|
|
104
|
+
* @param editionId - edition to project, or undefined for the full distribution.
|
|
105
|
+
* @returns the profile's dependencies, bundles, and override table.
|
|
106
|
+
*/
|
|
107
|
+
export declare function projectEdition(facts: DistributionFacts, editionId?: string, extraExcludes?: readonly string[]): ProfileProjection;
|
|
108
|
+
/**
|
|
109
|
+
* Assert every redirect target is served by a registry.
|
|
110
|
+
*
|
|
111
|
+
* A redirect names a published build; one that names a version the registry does not serve
|
|
112
|
+
* installs nothing at all, and the failure surfaces as a resolver error that reads like a
|
|
113
|
+
* packaging defect rather than a release that never happened.
|
|
114
|
+
*
|
|
115
|
+
* @param redirects - official name to `npm:` alias spec.
|
|
116
|
+
* @param registry - npm registry base URL.
|
|
117
|
+
* @returns nothing; throws on the first unavailable target.
|
|
118
|
+
*/
|
|
119
|
+
export declare function assertRedirectsPublished(redirects: Readonly<Record<string, string>>, registry?: string): Promise<void>;
|
|
120
|
+
/**
|
|
121
|
+
* Write the profile manifest and the pnpm workspace the install reads.
|
|
122
|
+
*
|
|
123
|
+
* npm reads `overrides` from the root manifest and pnpm reads them from the workspace file, so
|
|
124
|
+
* both are written from the same projection. Resolver settings a container needs (store location,
|
|
125
|
+
* supported architectures, release-age window) belong to the caller and are appended to the
|
|
126
|
+
* workspace file afterwards.
|
|
127
|
+
*
|
|
128
|
+
* @param directory - profile directory to write into.
|
|
129
|
+
* @param facts - validated distribution facts.
|
|
130
|
+
* @param editionId - edition to project.
|
|
131
|
+
* @param extraExcludes - packages the caller drops on top of the edition's own exclusions.
|
|
132
|
+
* @returns the projection that was written.
|
|
133
|
+
*/
|
|
134
|
+
export declare function writeProfileProjection(directory: string, facts: DistributionFacts, editionId?: string, extraExcludes?: readonly string[]): ProfileProjection;
|
|
135
|
+
/** Whether a path is a directory carrying a package manifest. */
|
|
136
|
+
export declare function isDistributionDirectory(directory: string): boolean;
|
|
137
|
+
//# sourceMappingURL=manifest.d.ts.map
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derive the profile's override table from the distribution's own facts.
|
|
3
|
+
*
|
|
4
|
+
* The distribution records one truth per fact: the patch set names the official packages it
|
|
5
|
+
* patches, the runtime names the version its republished builds carry, and an edition names the
|
|
6
|
+
* capabilities a deployment omits. The override table npm and pnpm read is a projection of those
|
|
7
|
+
* three, never a fourth list: an entry that cannot be derived fails the build instead of being
|
|
8
|
+
* carried forward, which is what kept a stale `@deepseek-ai/dsh-client-ui-slots` pin alive long
|
|
9
|
+
* after the patch behind it had gone.
|
|
10
|
+
*
|
|
11
|
+
* @module manifest
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
14
|
+
import { join, resolve } from 'node:path';
|
|
15
|
+
import { valid, validRange } from 'semver';
|
|
16
|
+
/** npm scope every patched official package is republished under. */
|
|
17
|
+
const OFFICIAL_PREFIX = '@deepseek-ai/';
|
|
18
|
+
/** npm scope the distribution publishes its republished builds under. */
|
|
19
|
+
const REPUBLISHED_PREFIX = '@sparkelf/';
|
|
20
|
+
function record(value, label) {
|
|
21
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
22
|
+
throw new Error(label + ' must be an object');
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
/** A declared value as text, or the fallback when the manifest holds something else. */
|
|
26
|
+
function text(value, fallback = '') {
|
|
27
|
+
return typeof value === 'string' ? value : fallback;
|
|
28
|
+
}
|
|
29
|
+
function stringArray(value, label) {
|
|
30
|
+
if (!Array.isArray(value) || value.some(entry => typeof entry !== 'string' || entry === '')) {
|
|
31
|
+
throw new Error(label + ' must be a non-empty string array');
|
|
32
|
+
}
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
function stringMap(value, label) {
|
|
36
|
+
const result = {};
|
|
37
|
+
for (const [name, spec] of Object.entries(record(value, label))) {
|
|
38
|
+
if (typeof spec !== 'string' || spec === '')
|
|
39
|
+
throw new Error(label + '.' + name + ' must be a non-empty string');
|
|
40
|
+
result[name] = spec;
|
|
41
|
+
}
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The republished scope name for one official package.
|
|
46
|
+
*
|
|
47
|
+
* @param official - an `@deepseek-ai/dsh-*` name.
|
|
48
|
+
* @returns the `@sparkelf/dsh-*` name the distribution publishes it as.
|
|
49
|
+
*/
|
|
50
|
+
export function republishedName(official) {
|
|
51
|
+
if (!official.startsWith(OFFICIAL_PREFIX))
|
|
52
|
+
throw new Error(official + ' is not an official package name');
|
|
53
|
+
return REPUBLISHED_PREFIX + official.slice(OFFICIAL_PREFIX.length);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Derive the redirect table from the patch set.
|
|
57
|
+
*
|
|
58
|
+
* Every `dsh-source` entry republishes the packages its payload modifies, because a registry
|
|
59
|
+
* installation has no source tree to patch. An entry that changes only types sets
|
|
60
|
+
* `target.republish: false` and must say why; anything else would look like a redirect that
|
|
61
|
+
* silently never happens.
|
|
62
|
+
*
|
|
63
|
+
* @param entries - the distribution's patch entries.
|
|
64
|
+
* @param runtimeVersion - exact version the republished builds carry.
|
|
65
|
+
* @returns official name to `npm:` alias spec.
|
|
66
|
+
*/
|
|
67
|
+
export function deriveRedirects(entries, runtimeVersion) {
|
|
68
|
+
const redirects = {};
|
|
69
|
+
const ids = {};
|
|
70
|
+
for (const entry of entries) {
|
|
71
|
+
if (entry.target.kind !== 'dsh-source')
|
|
72
|
+
continue;
|
|
73
|
+
const packages = entry.target.packages ?? [];
|
|
74
|
+
if (packages.length === 0)
|
|
75
|
+
throw new Error('patch entry ' + entry.id + ' declares no target packages');
|
|
76
|
+
const republish = entry.target.republish !== false;
|
|
77
|
+
if (!republish) {
|
|
78
|
+
if (entry.target.reason === undefined || entry.target.reason.trim() === '') {
|
|
79
|
+
throw new Error('patch entry ' + entry.id + ' sets republish:false without a reason');
|
|
80
|
+
}
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
for (const official of packages) {
|
|
84
|
+
const spec = 'npm:' + republishedName(official) + '@' + runtimeVersion;
|
|
85
|
+
if (redirects[official] !== undefined && redirects[official] !== spec) {
|
|
86
|
+
throw new Error('patch entries ' + (ids[official] ?? '?') + ' and ' + entry.id + ' redirect ' + official + ' to different targets');
|
|
87
|
+
}
|
|
88
|
+
redirects[official] = spec;
|
|
89
|
+
ids[official] = entry.id;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (Object.keys(redirects).length === 0)
|
|
93
|
+
throw new Error('the patch set declares no republishable target');
|
|
94
|
+
return { redirects, ids };
|
|
95
|
+
}
|
|
96
|
+
/** Read and validate the facts one installed distribution declares. */
|
|
97
|
+
export function readDistributionFacts(directory) {
|
|
98
|
+
const manifest = record(JSON.parse(readFileSync(join(directory, 'package.json'), 'utf8')), 'distribution manifest');
|
|
99
|
+
const plus = record(manifest.dshPlus, 'dshPlus');
|
|
100
|
+
const profile = record(plus.profile, 'dshPlus.profile');
|
|
101
|
+
if (profile.overrides !== undefined) {
|
|
102
|
+
// The table this module replaces. Keeping both is exactly the drift that shipped a bare
|
|
103
|
+
// version no patch produced, so a manifest that still carries one is rejected.
|
|
104
|
+
throw new Error('dshPlus.profile.overrides is retired; overrides are derived from dshPlus.patchSet');
|
|
105
|
+
}
|
|
106
|
+
const runtime = record(plus.runtime, 'dshPlus.runtime');
|
|
107
|
+
const runtimeVersion = text(runtime.version);
|
|
108
|
+
if (valid(runtimeVersion) === null)
|
|
109
|
+
throw new Error('dshPlus.runtime.version must be an exact version, got: ' + runtimeVersion);
|
|
110
|
+
const omission = record(plus.omission, 'dshPlus.omission');
|
|
111
|
+
const omissionPackage = text(omission.package);
|
|
112
|
+
const omissionVersion = text(omission.version);
|
|
113
|
+
if (!omissionPackage.startsWith('@sparkelf/') || valid(omissionVersion) === null) {
|
|
114
|
+
throw new Error('dshPlus.omission must name an @sparkelf package and an exact version');
|
|
115
|
+
}
|
|
116
|
+
const compatibility = record(plus.compatibility, 'dshPlus.compatibility');
|
|
117
|
+
const floor = text(compatibility.dsh);
|
|
118
|
+
if (validRange(floor) === null)
|
|
119
|
+
throw new Error('dshPlus.compatibility.dsh must be a semver range');
|
|
120
|
+
const patchSet = record(plus.patchSet, 'dshPlus.patchSet');
|
|
121
|
+
const rawEntries = patchSet.entries;
|
|
122
|
+
if (!Array.isArray(rawEntries) || rawEntries.length === 0)
|
|
123
|
+
throw new Error('dshPlus.patchSet.entries must be a non-empty array');
|
|
124
|
+
const entries = rawEntries.map((value, index) => {
|
|
125
|
+
const entry = record(value, 'patchSet.entries[' + String(index) + ']');
|
|
126
|
+
const target = record(entry.target, 'patchSet.entries[' + String(index) + '].target');
|
|
127
|
+
const targetFacts = { kind: text(target.kind) };
|
|
128
|
+
if (target.packages !== undefined) {
|
|
129
|
+
Object.assign(targetFacts, { packages: stringArray(target.packages, 'target.packages') });
|
|
130
|
+
}
|
|
131
|
+
if (target.republish === false)
|
|
132
|
+
Object.assign(targetFacts, { republish: false });
|
|
133
|
+
if (typeof target.reason === 'string')
|
|
134
|
+
Object.assign(targetFacts, { reason: target.reason });
|
|
135
|
+
return { id: text(entry.id), target: targetFacts };
|
|
136
|
+
});
|
|
137
|
+
const { redirects, ids } = deriveRedirects(entries, runtimeVersion);
|
|
138
|
+
const dependencies = stringMap(profile.dependencies, 'dshPlus.profile.dependencies');
|
|
139
|
+
const allowBuilds = {};
|
|
140
|
+
for (const [name, allowed] of Object.entries(record(profile.allowBuilds, 'dshPlus.profile.allowBuilds'))) {
|
|
141
|
+
if (typeof allowed !== 'boolean')
|
|
142
|
+
throw new Error('dshPlus.profile.allowBuilds.' + name + ' must be a boolean');
|
|
143
|
+
allowBuilds[name] = allowed;
|
|
144
|
+
}
|
|
145
|
+
const editions = {};
|
|
146
|
+
const rawEditions = plus.editions === undefined ? {} : record(plus.editions, 'dshPlus.editions');
|
|
147
|
+
for (const [id, value] of Object.entries(rawEditions)) {
|
|
148
|
+
const edition = record(value, 'dshPlus.editions.' + id);
|
|
149
|
+
// Both shapes are read: an object maps names to versions (the historic form), an array names
|
|
150
|
+
// them. Only the names matter now -- the version is `profile.dependencies`' job.
|
|
151
|
+
const rawPackages = edition.packages;
|
|
152
|
+
if (rawPackages === undefined)
|
|
153
|
+
throw new Error('dshPlus.editions.' + id + '.packages is required');
|
|
154
|
+
const names = Array.isArray(rawPackages)
|
|
155
|
+
? stringArray(rawPackages, 'dshPlus.editions.' + id + '.packages')
|
|
156
|
+
: Object.keys(record(rawPackages, 'dshPlus.editions.' + id + '.packages'));
|
|
157
|
+
for (const name of names) {
|
|
158
|
+
if (dependencyKey(dependencies, name) === undefined) {
|
|
159
|
+
throw new Error('edition "' + id + '" installs ' + name + ', which dshPlus.profile.dependencies does not pin');
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const omit = stringArray(edition.omit ?? [], 'dshPlus.editions.' + id + '.omit');
|
|
163
|
+
for (const name of omit) {
|
|
164
|
+
if (dependencies[name] === undefined && manifest.dependencies?.[name] === undefined) {
|
|
165
|
+
throw new Error('edition "' + id + '" omits ' + name + ', which the distribution does not declare');
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const excludePackages = stringArray(edition.excludePackages ?? [], 'dshPlus.editions.' + id + '.excludePackages');
|
|
169
|
+
for (const name of excludePackages) {
|
|
170
|
+
if (dependencies[name] === undefined && !stringArray(edition.bundles ?? [], 'x').includes(name)) {
|
|
171
|
+
throw new Error('edition "' + id + '" excludes ' + name + ', which it does not install or mount');
|
|
172
|
+
}
|
|
173
|
+
if (omit.includes(name))
|
|
174
|
+
throw new Error('edition "' + id + '" both excludes and omits ' + name);
|
|
175
|
+
// A hard dependency would keep the package in the tree through another spec, so the
|
|
176
|
+
// exclusion would look applied while the install still fetched it.
|
|
177
|
+
for (const [other, spec] of Object.entries(dependencies)) {
|
|
178
|
+
if (/^npm:/u.test(spec) && spec.includes(name + '@')) {
|
|
179
|
+
throw new Error('edition "' + id + '" excludes ' + name + ' but ' + other + ' aliases it');
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const bundles = stringArray(edition.bundles ?? [], 'dshPlus.editions.' + id + '.bundles');
|
|
184
|
+
// A bundle row whose package is substituted by the omission placeholder cannot mount: the
|
|
185
|
+
// loader reads the installed manifest and finds a package that declares nothing. The two
|
|
186
|
+
// lists describe one fact, so an overlap is a generation error rather than a build surprise.
|
|
187
|
+
for (const name of bundles) {
|
|
188
|
+
if (omit.includes(name))
|
|
189
|
+
throw new Error('edition "' + id + '" mounts ' + name + ' but also omits it');
|
|
190
|
+
}
|
|
191
|
+
editions[id] = {
|
|
192
|
+
label: text(edition.label, id),
|
|
193
|
+
profile: text(edition.profile, 'plus'),
|
|
194
|
+
bundles,
|
|
195
|
+
packages: names,
|
|
196
|
+
omit,
|
|
197
|
+
excludePackages,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
name: String(manifest.name),
|
|
202
|
+
version: String(manifest.version),
|
|
203
|
+
runtimeVersion,
|
|
204
|
+
omissionSpec: 'npm:' + omissionPackage + '@' + omissionVersion,
|
|
205
|
+
bundles: stringArray(profile.bundles, 'dshPlus.profile.bundles'),
|
|
206
|
+
dependencies,
|
|
207
|
+
allowBuilds,
|
|
208
|
+
redirects,
|
|
209
|
+
redirectIds: ids,
|
|
210
|
+
editions,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* The dependency-table key that installs one package.
|
|
215
|
+
*
|
|
216
|
+
* A bundle published under a scope we own is installed through an alias: the official name a
|
|
217
|
+
* consumer imports (`dsh-better-sidebar`) is the key, and the spec names our build
|
|
218
|
+
* (`npm:@sparkelf/dsh-better-sidebar@…`). Editions name the real package, so the
|
|
219
|
+
* lookup has to follow the alias or it reports a reviewed package as unpinned.
|
|
220
|
+
*
|
|
221
|
+
* @param dependencies - the distribution's dependency table.
|
|
222
|
+
* @param name - package name an edition names.
|
|
223
|
+
* @returns the key that installs it, or undefined when the distribution does not carry it.
|
|
224
|
+
*/
|
|
225
|
+
function dependencyKey(dependencies, name) {
|
|
226
|
+
if (dependencies[name] !== undefined)
|
|
227
|
+
return name;
|
|
228
|
+
for (const [key, spec] of Object.entries(dependencies)) {
|
|
229
|
+
const alias = /^npm:(@[^/]+\/[^@]+)@/u.exec(spec);
|
|
230
|
+
if (alias?.[1] === name)
|
|
231
|
+
return key;
|
|
232
|
+
}
|
|
233
|
+
return undefined;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Project an edition into the profile npm and pnpm install.
|
|
237
|
+
*
|
|
238
|
+
* The distribution's reviewed order is the base; the edition names the bundle order and package
|
|
239
|
+
* set it installs and the capabilities it must not run. Versions always come from
|
|
240
|
+
* `profile.dependencies`, so an edition cannot pin a version the distribution did not review.
|
|
241
|
+
* No edition means the full distribution, which is what a launcher start without a selection
|
|
242
|
+
* materializes.
|
|
243
|
+
*
|
|
244
|
+
* @param facts - validated distribution facts.
|
|
245
|
+
* @param editionId - edition to project, or undefined for the full distribution.
|
|
246
|
+
* @returns the profile's dependencies, bundles, and override table.
|
|
247
|
+
*/
|
|
248
|
+
export function projectEdition(facts, editionId, extraExcludes = []) {
|
|
249
|
+
const edition = editionId === undefined ? undefined : facts.editions[editionId];
|
|
250
|
+
if (editionId !== undefined && edition === undefined) {
|
|
251
|
+
throw new Error('unknown edition "' + editionId + '"; declared: ' + (Object.keys(facts.editions).join(', ') || '(none)'));
|
|
252
|
+
}
|
|
253
|
+
const excludedNames = new Set([...(edition?.excludePackages ?? []), ...extraExcludes]);
|
|
254
|
+
for (const name of excludedNames) {
|
|
255
|
+
const declared = facts.dependencies[name] !== undefined || facts.bundles.includes(name);
|
|
256
|
+
if (!declared)
|
|
257
|
+
throw new Error('exclude names ' + name + ', which the distribution does not install or mount');
|
|
258
|
+
}
|
|
259
|
+
const omittedNames = new Set(edition?.omit ?? []);
|
|
260
|
+
const omitted = {};
|
|
261
|
+
// The placeholder is a plus-family member on its own version line, so its release identity is
|
|
262
|
+
// stated once in `dshPlus.omission` rather than derived from a version that moves elsewhere.
|
|
263
|
+
for (const name of omittedNames)
|
|
264
|
+
omitted[name] = facts.omissionSpec;
|
|
265
|
+
const dependencies = {};
|
|
266
|
+
// Official services are shared infrastructure rather than edition members: every edition
|
|
267
|
+
// installs the ones the distribution reviewed, which is what keeps one version of each service
|
|
268
|
+
// in the tree. An edition lists only the product packages it owns.
|
|
269
|
+
const sharedOfficials = Object.keys(facts.dependencies).filter(name => name.startsWith('@deepseek-ai/dsh'));
|
|
270
|
+
const packageNames = edition === undefined
|
|
271
|
+
? [...new Set([...Object.keys(facts.dependencies), ...sharedOfficials])]
|
|
272
|
+
: [...new Set([...edition.packages, ...sharedOfficials])];
|
|
273
|
+
for (const name of packageNames) {
|
|
274
|
+
if (omittedNames.has(name) || excludedNames.has(name))
|
|
275
|
+
continue;
|
|
276
|
+
const key = dependencyKey(facts.dependencies, name) ?? name;
|
|
277
|
+
const spec = facts.dependencies[key];
|
|
278
|
+
if (spec === undefined)
|
|
279
|
+
throw new Error(name + ' is not a reviewed package');
|
|
280
|
+
dependencies[key] = spec;
|
|
281
|
+
}
|
|
282
|
+
// A redirect is how a registry installation receives a patched build; the profile has to
|
|
283
|
+
// declare it under the official name it imports as well as override it.
|
|
284
|
+
for (const [name, spec] of Object.entries(facts.redirects)) {
|
|
285
|
+
if (omittedNames.has(name) || excludedNames.has(name))
|
|
286
|
+
continue;
|
|
287
|
+
dependencies[name] = spec;
|
|
288
|
+
}
|
|
289
|
+
const bundles = (edition === undefined
|
|
290
|
+
? [...facts.bundles]
|
|
291
|
+
: [...new Set(edition.bundles)]).filter(name => !excludedNames.has(name));
|
|
292
|
+
// Bundle rows resolve from the profile tree, which also carries the runtime's own closure, so
|
|
293
|
+
// their presence is asserted against the installed tree after the install rather than here.
|
|
294
|
+
for (const name of bundles) {
|
|
295
|
+
if (!facts.bundles.includes(name) && !(edition?.bundles ?? []).includes(name)) {
|
|
296
|
+
throw new Error('edition "' + String(editionId) + '" mounts bundle ' + name + ', which the distribution does not declare');
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
const overrides = {};
|
|
300
|
+
for (const [name, spec] of Object.entries(facts.redirects)) {
|
|
301
|
+
if (omittedNames.has(name) || excludedNames.has(name))
|
|
302
|
+
continue;
|
|
303
|
+
overrides[name] = spec;
|
|
304
|
+
}
|
|
305
|
+
// The launcher is the runtime the republished builds were cut against. A dependency can lose
|
|
306
|
+
// to hoisting when a sibling range admits a newer prerelease, and a mixed tree is what makes
|
|
307
|
+
// DSH refuse rows; the override is derived from the same one fact, not a second list.
|
|
308
|
+
overrides['@deepseek-ai/dsh'] = facts.runtimeVersion;
|
|
309
|
+
Object.assign(overrides, omitted);
|
|
310
|
+
return {
|
|
311
|
+
profileName: edition?.profile ?? 'plus',
|
|
312
|
+
dependencies,
|
|
313
|
+
bundles,
|
|
314
|
+
overrides,
|
|
315
|
+
allowBuilds: facts.allowBuilds,
|
|
316
|
+
omittedPackages: omitted,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Assert every redirect target is served by a registry.
|
|
321
|
+
*
|
|
322
|
+
* A redirect names a published build; one that names a version the registry does not serve
|
|
323
|
+
* installs nothing at all, and the failure surfaces as a resolver error that reads like a
|
|
324
|
+
* packaging defect rather than a release that never happened.
|
|
325
|
+
*
|
|
326
|
+
* @param redirects - official name to `npm:` alias spec.
|
|
327
|
+
* @param registry - npm registry base URL.
|
|
328
|
+
* @returns nothing; throws on the first unavailable target.
|
|
329
|
+
*/
|
|
330
|
+
export async function assertRedirectsPublished(redirects, registry = 'https://registry.npmjs.org') {
|
|
331
|
+
for (const spec of Object.values(redirects)) {
|
|
332
|
+
const match = /^npm:(@[^/]+\/[^@]+)@(.+)$/u.exec(spec);
|
|
333
|
+
if (match === null)
|
|
334
|
+
throw new Error('redirect ' + spec + ' is not an npm alias');
|
|
335
|
+
const [, name, version] = match;
|
|
336
|
+
if (name === undefined || version === undefined)
|
|
337
|
+
throw new Error('redirect ' + spec + ' is not an npm alias');
|
|
338
|
+
const response = await fetch(registry.replace(/\/$/u, '') + '/' + name.replace('/', '%2f') + '/' + version);
|
|
339
|
+
if (!response.ok)
|
|
340
|
+
throw new Error('redirect target ' + name + '@' + version + ' is not served by ' + registry);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Write the profile manifest and the pnpm workspace the install reads.
|
|
345
|
+
*
|
|
346
|
+
* npm reads `overrides` from the root manifest and pnpm reads them from the workspace file, so
|
|
347
|
+
* both are written from the same projection. Resolver settings a container needs (store location,
|
|
348
|
+
* supported architectures, release-age window) belong to the caller and are appended to the
|
|
349
|
+
* workspace file afterwards.
|
|
350
|
+
*
|
|
351
|
+
* @param directory - profile directory to write into.
|
|
352
|
+
* @param facts - validated distribution facts.
|
|
353
|
+
* @param editionId - edition to project.
|
|
354
|
+
* @param extraExcludes - packages the caller drops on top of the edition's own exclusions.
|
|
355
|
+
* @returns the projection that was written.
|
|
356
|
+
*/
|
|
357
|
+
export function writeProfileProjection(directory, facts, editionId, extraExcludes = []) {
|
|
358
|
+
const projection = projectEdition(facts, editionId, extraExcludes);
|
|
359
|
+
mkdirSync(directory, { recursive: true });
|
|
360
|
+
const dependencies = {
|
|
361
|
+
'@sparkelf/dsh-plus': facts.version,
|
|
362
|
+
// Exact: the republished builds were cut against this runtime, and DSH refuses a row whose
|
|
363
|
+
// exact peer pins disagree with the process it shares. A range admits the next prerelease and
|
|
364
|
+
// reintroduces the skew the redirects exist to avoid.
|
|
365
|
+
'@deepseek-ai/dsh': facts.runtimeVersion,
|
|
366
|
+
...projection.dependencies,
|
|
367
|
+
};
|
|
368
|
+
writeFileSync(join(directory, 'package.json'), JSON.stringify({
|
|
369
|
+
name: 'dsh-profile-' + projection.profileName,
|
|
370
|
+
private: true,
|
|
371
|
+
type: 'module',
|
|
372
|
+
dependencies,
|
|
373
|
+
dsh: { profile: { bundles: projection.bundles, patchReload: 'live' } },
|
|
374
|
+
}, null, 2) + '\n');
|
|
375
|
+
const workspace = [
|
|
376
|
+
'packages:',
|
|
377
|
+
' - .',
|
|
378
|
+
'nodeLinker: hoisted',
|
|
379
|
+
// Official peers come from the exact runtime this profile declares. Installing them from
|
|
380
|
+
// their own ranges lets a newer prerelease in beside the republished builds, and DSH then
|
|
381
|
+
// disables every row whose exact peers disagree with the runtime it boots.
|
|
382
|
+
'autoInstallPeers: false',
|
|
383
|
+
'',
|
|
384
|
+
'overrides:',
|
|
385
|
+
...Object.keys(projection.overrides).sort().map(name => ' ' + JSON.stringify(name) + ': ' + (projection.overrides[name] ?? '')),
|
|
386
|
+
'',
|
|
387
|
+
'allowBuilds:',
|
|
388
|
+
...Object.keys(projection.allowBuilds).sort().map(name => ' ' + JSON.stringify(name) + ': ' + String(projection.allowBuilds[name])),
|
|
389
|
+
'',
|
|
390
|
+
];
|
|
391
|
+
writeFileSync(join(directory, 'pnpm-workspace.yaml'), workspace.join('\n'));
|
|
392
|
+
return projection;
|
|
393
|
+
}
|
|
394
|
+
/** Whether a path is a directory carrying a package manifest. */
|
|
395
|
+
export function isDistributionDirectory(directory) {
|
|
396
|
+
return existsSync(join(resolve(directory), 'package.json'));
|
|
397
|
+
}
|
|
398
|
+
//# sourceMappingURL=manifest.js.map
|