@sparkelf/dsh-plus 0.2.1-rc.2.4 → 0.2.2-alpha.2.2
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/README.md +7 -5
- package/README.zh.md +7 -5
- package/lib/apply-08huHfLG.js +665 -0
- package/lib/apply-DmICvIGh.js +653 -0
- package/lib/apply.js +1 -1
- package/lib/bin.js +188 -45
- package/lib/types/apply.d.ts +1 -1
- package/lib/types/apply.js +56 -117
- package/lib/types/patch-set.d.ts +59 -0
- package/lib/types/patch-set.js +132 -0
- package/lib/types/standalone-cli.js +93 -2
- package/lib/types/standalone-profile.d.ts +8 -1
- package/lib/types/standalone-profile.js +126 -49
- package/package.json +602 -175
- package/patches/approval-allow-policy.patch +108 -0
- package/patches/atomic-write-zombie-lock-holder.patch +53 -0
- package/patches/bootstrap-exempt-names.patch +32 -0
- package/patches/browser-auth-mode.patch +127 -0
- package/patches/composer-popover-boundaries.patch +337 -0
- package/patches/diagram-preview.patch +562 -0
- package/patches/image-offload-hoist-replay.patch +533 -0
- package/patches/legacy-code-preset.patch +148 -0
- package/patches/llm-pi-ai-transport-truncation.patch +33 -0
- package/patches/mobile-journal-generation.patch +76 -0
- package/patches/office-preview-stable-load.patch +34 -0
- package/patches/officecli-deliverables.patch +42 -0
- package/patches/ptc-mcp-schema-types.patch +156 -0
- package/patches/responses-reasoning-status.patch +289 -0
- package/patches/session-envelope-ignorable.patch +63 -0
- package/patches/session-export-chinese.patch +85 -0
- package/patches/session-format-legacy-restart.patch +35 -0
- package/patches/session-log-trajectory-toolbar.patch +352 -0
- package/patches/state-dot-brand-blue.patch +185 -0
- package/patches/ui-settings-compact-phone.patch +76 -0
- package/patches/web-runtime-compat.patch +83 -0
- package/patches/workspace-storage-restore.patch +186 -0
- package/patches/wsl-native-open.patch +265 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read the Plus distribution's embedded patch set.
|
|
3
|
+
*
|
|
4
|
+
* The distribution owns one patch set for official DSH and, separately, the patches it
|
|
5
|
+
* carries for third-party packages. Both live inside the distribution package: the
|
|
6
|
+
* manifest declares entries, and each entry's payload is a file under `patches/`. A
|
|
7
|
+
* consumer resolves no independently published patch package.
|
|
8
|
+
*
|
|
9
|
+
* The two lists are governed differently. A Plus entry must target an official package
|
|
10
|
+
* that the profile republishes and overrides; a third-party entry must name its upstream
|
|
11
|
+
* and a retirement condition, and never becomes part of the official patch set.
|
|
12
|
+
*/
|
|
13
|
+
/** An npm package the distribution patches in place. */
|
|
14
|
+
export interface NpmPatchTarget {
|
|
15
|
+
readonly kind: 'npm';
|
|
16
|
+
readonly name: string;
|
|
17
|
+
readonly range: string;
|
|
18
|
+
}
|
|
19
|
+
/** One official source path set, applied to the exact `dshPlus.sourceBase` revision. */
|
|
20
|
+
export interface SourcePatchTarget {
|
|
21
|
+
readonly kind: 'dsh-source';
|
|
22
|
+
readonly baseRevision: string;
|
|
23
|
+
/** Official packages the patch modifies; each must be republished and overridden. */
|
|
24
|
+
readonly packages: readonly string[];
|
|
25
|
+
readonly paths: readonly string[];
|
|
26
|
+
}
|
|
27
|
+
/** Where one patch payload lands. */
|
|
28
|
+
export type PatchTarget = NpmPatchTarget | SourcePatchTarget;
|
|
29
|
+
/** Who owns the entry: the Plus official delta or a carried third-party patch. */
|
|
30
|
+
export type PatchScope = 'plus' | 'third-party';
|
|
31
|
+
/** One entry of the embedded patch set. */
|
|
32
|
+
export interface PatchSetEntry {
|
|
33
|
+
/** Stable identity used by the lock and by retirement records. */
|
|
34
|
+
readonly id: string;
|
|
35
|
+
readonly scope: PatchScope;
|
|
36
|
+
/** DSH range the entry applies to. */
|
|
37
|
+
readonly dsh: string;
|
|
38
|
+
/** Payload path, relative to the distribution package. */
|
|
39
|
+
readonly file: string;
|
|
40
|
+
/** The behavior or upstream release that retires this entry. */
|
|
41
|
+
readonly retireWhen: string;
|
|
42
|
+
/** Third-party entries name the upstream they track. */
|
|
43
|
+
readonly upstreamUrl?: string;
|
|
44
|
+
readonly target: PatchTarget;
|
|
45
|
+
}
|
|
46
|
+
/** The distribution's complete embedded patch declaration. */
|
|
47
|
+
export interface PatchSet {
|
|
48
|
+
readonly entries: readonly PatchSetEntry[];
|
|
49
|
+
readonly thirdPartyPatches: readonly PatchSetEntry[];
|
|
50
|
+
readonly sourceBaseRevision: string;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Read and validate the embedded patch set from an installed distribution.
|
|
54
|
+
* @param distributionDirectory - installed `@sparkelf/dsh-plus` directory.
|
|
55
|
+
* @param manifest - its parsed package manifest.
|
|
56
|
+
* @returns the ordered Plus entries, third-party entries, and the exact source base.
|
|
57
|
+
*/
|
|
58
|
+
export declare function readPatchSet(distributionDirectory: string, manifest: Record<string, unknown>): PatchSet;
|
|
59
|
+
//# sourceMappingURL=patch-set.d.ts.map
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read the Plus distribution's embedded patch set.
|
|
3
|
+
*
|
|
4
|
+
* The distribution owns one patch set for official DSH and, separately, the patches it
|
|
5
|
+
* carries for third-party packages. Both live inside the distribution package: the
|
|
6
|
+
* manifest declares entries, and each entry's payload is a file under `patches/`. A
|
|
7
|
+
* consumer resolves no independently published patch package.
|
|
8
|
+
*
|
|
9
|
+
* The two lists are governed differently. A Plus entry must target an official package
|
|
10
|
+
* that the profile republishes and overrides; a third-party entry must name its upstream
|
|
11
|
+
* and a retirement condition, and never becomes part of the official patch set.
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync } from 'node:fs';
|
|
14
|
+
import { isAbsolute, relative, resolve } from 'node:path';
|
|
15
|
+
import { validRange } from 'semver';
|
|
16
|
+
function object(value, label) {
|
|
17
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
18
|
+
throw new Error(label + ' must be an object');
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
function string(value, label) {
|
|
22
|
+
if (typeof value !== 'string' || value === '')
|
|
23
|
+
throw new Error(label + ' must be a non-empty string');
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
function range(value, label) {
|
|
27
|
+
const text = string(value, label);
|
|
28
|
+
if (validRange(text) === null)
|
|
29
|
+
throw new Error(label + ' is not a semantic-version range: ' + text);
|
|
30
|
+
return text;
|
|
31
|
+
}
|
|
32
|
+
/** A payload stays inside the distribution package it is declared by. */
|
|
33
|
+
function patchFile(distributionDirectory, declared, label) {
|
|
34
|
+
const absolute = resolve(distributionDirectory, declared);
|
|
35
|
+
const local = relative(distributionDirectory, absolute);
|
|
36
|
+
if (local === '' || isAbsolute(local) || local === '..' || local.startsWith('../')) {
|
|
37
|
+
throw new Error(label + ' must stay inside the distribution package: ' + declared);
|
|
38
|
+
}
|
|
39
|
+
if (!existsSync(absolute))
|
|
40
|
+
throw new Error(label + ' does not exist: ' + absolute);
|
|
41
|
+
return local.replaceAll('\\', '/');
|
|
42
|
+
}
|
|
43
|
+
function sourceTarget(value, sourceBaseRevision, label) {
|
|
44
|
+
const target = object(value, label);
|
|
45
|
+
if (target.kind !== 'dsh-source')
|
|
46
|
+
throw new Error(label + '.kind must be dsh-source');
|
|
47
|
+
if (!Array.isArray(target.paths) || target.paths.length === 0)
|
|
48
|
+
throw new Error(label + '.paths must be a non-empty array');
|
|
49
|
+
const paths = target.paths.map((entry, index) => {
|
|
50
|
+
const path = string(entry, label + '.paths[' + String(index) + ']');
|
|
51
|
+
if (!path.endsWith('/'))
|
|
52
|
+
throw new Error(label + '.paths entries must be repository directory prefixes');
|
|
53
|
+
return path;
|
|
54
|
+
});
|
|
55
|
+
if (!Array.isArray(target.packages) || target.packages.length === 0) {
|
|
56
|
+
throw new Error(label + '.packages must be a non-empty array');
|
|
57
|
+
}
|
|
58
|
+
const packages = target.packages.map((entry, index) => {
|
|
59
|
+
const name = string(entry, label + '.packages[' + String(index) + ']');
|
|
60
|
+
if (!name.startsWith('@deepseek-ai/'))
|
|
61
|
+
throw new Error(label + '.packages entries must be official package names');
|
|
62
|
+
return name;
|
|
63
|
+
});
|
|
64
|
+
return { kind: 'dsh-source', baseRevision: sourceBaseRevision, packages, paths };
|
|
65
|
+
}
|
|
66
|
+
function npmTarget(value, label) {
|
|
67
|
+
const target = object(value, label);
|
|
68
|
+
if (target.kind !== 'npm')
|
|
69
|
+
throw new Error(label + '.kind must be npm');
|
|
70
|
+
return {
|
|
71
|
+
kind: 'npm',
|
|
72
|
+
name: string(target.name, label + '.name'),
|
|
73
|
+
range: range(target.range, label + '.range'),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function entry(value, scope, sourceBaseRevision, distributionDirectory, label) {
|
|
77
|
+
const record = object(value, label);
|
|
78
|
+
const id = string(record.id, label + '.id');
|
|
79
|
+
const dsh = range(record.dsh, label + '.dsh');
|
|
80
|
+
const file = patchFile(distributionDirectory, string(record.file, label + '.file'), label + '.file');
|
|
81
|
+
const retireWhen = string(record.retireWhen, label + '.retireWhen');
|
|
82
|
+
const targetLabel = label + '.target';
|
|
83
|
+
const target = scope === 'plus'
|
|
84
|
+
? (object(record.target, targetLabel).kind === 'dsh-source'
|
|
85
|
+
? sourceTarget(record.target, sourceBaseRevision, targetLabel)
|
|
86
|
+
: npmTarget(record.target, targetLabel))
|
|
87
|
+
: npmTarget(record.target, targetLabel);
|
|
88
|
+
if (scope === 'third-party') {
|
|
89
|
+
return {
|
|
90
|
+
id,
|
|
91
|
+
scope,
|
|
92
|
+
dsh,
|
|
93
|
+
file,
|
|
94
|
+
retireWhen,
|
|
95
|
+
upstreamUrl: string(record.upstreamUrl, label + '.upstreamUrl'),
|
|
96
|
+
target,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
return { id, scope, dsh, file, retireWhen, target };
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Read and validate the embedded patch set from an installed distribution.
|
|
103
|
+
* @param distributionDirectory - installed `@sparkelf/dsh-plus` directory.
|
|
104
|
+
* @param manifest - its parsed package manifest.
|
|
105
|
+
* @returns the ordered Plus entries, third-party entries, and the exact source base.
|
|
106
|
+
*/
|
|
107
|
+
export function readPatchSet(distributionDirectory, manifest) {
|
|
108
|
+
const plus = object(manifest.dshPlus, 'dshPlus');
|
|
109
|
+
const sourceBase = object(plus.sourceBase, 'dshPlus.sourceBase');
|
|
110
|
+
const sourceBaseRevision = string(sourceBase.revision, 'dshPlus.sourceBase.revision');
|
|
111
|
+
const declaration = object(plus.patchSet, 'dshPlus.patchSet');
|
|
112
|
+
if (declaration.formatVersion !== 1)
|
|
113
|
+
throw new Error('dshPlus.patchSet.formatVersion must be 1');
|
|
114
|
+
if (!Array.isArray(declaration.entries) || declaration.entries.length === 0) {
|
|
115
|
+
throw new Error('dshPlus.patchSet.entries must be a non-empty array');
|
|
116
|
+
}
|
|
117
|
+
const entries = declaration.entries.map((value, index) => entry(value, 'plus', sourceBaseRevision, distributionDirectory, 'dshPlus.patchSet.entries[' + String(index) + ']'));
|
|
118
|
+
const thirdPartyRaw = plus.thirdPartyPatches;
|
|
119
|
+
const thirdParty = thirdPartyRaw === undefined
|
|
120
|
+
? []
|
|
121
|
+
: (Array.isArray(thirdPartyRaw)
|
|
122
|
+
? thirdPartyRaw.map((value, index) => entry(value, 'third-party', sourceBaseRevision, distributionDirectory, 'dshPlus.thirdPartyPatches[' + String(index) + ']'))
|
|
123
|
+
: (() => { throw new Error('dshPlus.thirdPartyPatches must be an array'); })());
|
|
124
|
+
const ids = new Set();
|
|
125
|
+
for (const item of [...entries, ...thirdParty]) {
|
|
126
|
+
if (ids.has(item.id))
|
|
127
|
+
throw new Error('duplicate patch entry id: ' + item.id);
|
|
128
|
+
ids.add(item.id);
|
|
129
|
+
}
|
|
130
|
+
return { entries, thirdPartyPatches: thirdParty, sourceBaseRevision };
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=patch-set.js.map
|
|
@@ -25,6 +25,7 @@ function parseStartOptions(argv) {
|
|
|
25
25
|
let host = '127.0.0.1';
|
|
26
26
|
let open = true;
|
|
27
27
|
let foreground = false;
|
|
28
|
+
let edition;
|
|
28
29
|
let capabilities;
|
|
29
30
|
for (let index = 0; index < argv.length; index += 1) {
|
|
30
31
|
const token = argv[index];
|
|
@@ -52,6 +53,14 @@ function parseStartOptions(argv) {
|
|
|
52
53
|
foreground = true;
|
|
53
54
|
continue;
|
|
54
55
|
}
|
|
56
|
+
if (token === '--edition') {
|
|
57
|
+
const value = argv[index + 1];
|
|
58
|
+
if (value === undefined || value === '')
|
|
59
|
+
throw new Error('--edition requires a value');
|
|
60
|
+
edition = value;
|
|
61
|
+
index += 1;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
55
64
|
if (token === '--capabilities') {
|
|
56
65
|
const value = argv[index + 1];
|
|
57
66
|
if (value === undefined)
|
|
@@ -73,7 +82,7 @@ function parseStartOptions(argv) {
|
|
|
73
82
|
}
|
|
74
83
|
throw new Error('unknown option: ' + String(token));
|
|
75
84
|
}
|
|
76
|
-
return { port, host, open, foreground, capabilities };
|
|
85
|
+
return { port, host, open, foreground, edition, capabilities };
|
|
77
86
|
}
|
|
78
87
|
/**
|
|
79
88
|
* Resolve a stated capability selection.
|
|
@@ -157,6 +166,54 @@ function runForeground(entry, profileName, port, host, open) {
|
|
|
157
166
|
const result = spawnSync(process.execPath, args, { stdio: 'inherit' });
|
|
158
167
|
return result.status ?? 1;
|
|
159
168
|
}
|
|
169
|
+
/** Parse the install command's edition selection. */
|
|
170
|
+
function parseInstallOptions(argv) {
|
|
171
|
+
let edition;
|
|
172
|
+
let profile;
|
|
173
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
174
|
+
const token = argv[index];
|
|
175
|
+
if (token === '--edition') {
|
|
176
|
+
const value = argv[index + 1];
|
|
177
|
+
if (value === undefined || value === '')
|
|
178
|
+
throw new Error('--edition requires a value');
|
|
179
|
+
edition = value;
|
|
180
|
+
index += 1;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (token === '--profile') {
|
|
184
|
+
const value = argv[index + 1];
|
|
185
|
+
if (value === undefined || value === '')
|
|
186
|
+
throw new Error('--profile requires a value');
|
|
187
|
+
profile = value;
|
|
188
|
+
index += 1;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
throw new Error('unknown option: ' + String(token));
|
|
192
|
+
}
|
|
193
|
+
if (edition === undefined)
|
|
194
|
+
throw new Error('install requires --edition personal or --edition team');
|
|
195
|
+
return { edition, profile };
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Install one edition's package manifest into the profile.
|
|
199
|
+
*
|
|
200
|
+
* The profile's `package.json` is the install artifact; there is no separate standalone
|
|
201
|
+
* package. A later `start` uses the same manifest, so changing the edition is a new
|
|
202
|
+
* install rather than a second product.
|
|
203
|
+
*
|
|
204
|
+
* @param argv - arguments after `install`.
|
|
205
|
+
* @returns the process exit code.
|
|
206
|
+
*/
|
|
207
|
+
function install(argv) {
|
|
208
|
+
const { edition, profile } = parseInstallOptions(argv);
|
|
209
|
+
const base = resolvePaths(declarationAnchor());
|
|
210
|
+
const paths = profile === undefined
|
|
211
|
+
? base
|
|
212
|
+
: { ...base, profileName: profile, profileDirectory: join(base.home, 'profiles', profile) };
|
|
213
|
+
ensureProfile(paths, installationRoot(), edition);
|
|
214
|
+
console.log('Installed the ' + edition + ' edition into ' + paths.profileDirectory);
|
|
215
|
+
return 0;
|
|
216
|
+
}
|
|
160
217
|
async function start(argv) {
|
|
161
218
|
const options = parseStartOptions(argv);
|
|
162
219
|
const anchor = installationAnchor();
|
|
@@ -202,10 +259,12 @@ async function start(argv) {
|
|
|
202
259
|
}
|
|
203
260
|
console.log('pnpm installed.');
|
|
204
261
|
}
|
|
205
|
-
const created = ensureProfile(paths, installationRoot());
|
|
262
|
+
const created = ensureProfile(paths, installationRoot(), options.edition);
|
|
206
263
|
console.log(created
|
|
207
264
|
? 'Created the ' + paths.profileName + ' profile at ' + paths.profileDirectory
|
|
208
265
|
: 'Using the existing ' + paths.profileName + ' profile');
|
|
266
|
+
if (options.edition !== undefined)
|
|
267
|
+
console.log('Edition: ' + options.edition);
|
|
209
268
|
// The profile symlinks the consumer's packages, so a patch lands on the installed
|
|
210
269
|
// copy the launcher loads. A reinstall restores the published bytes, which is why
|
|
211
270
|
// this runs on every start rather than only when the profile was created.
|
|
@@ -581,6 +640,30 @@ async function doctor() {
|
|
|
581
640
|
? 'capabilities: ' + (actual.mounted.length === 0 ? '(none enabled)' : actual.mounted.join(', '))
|
|
582
641
|
: 'capabilities disagree with ' + CAPABILITY_RECORD + ': ' + drifted.map(capability => capability.id).join(', ')
|
|
583
642
|
+ ' (run dsh-plus capabilities)');
|
|
643
|
+
const profileManifestPath = join(paths.profileDirectory, 'package.json');
|
|
644
|
+
if (existsSync(profileManifestPath)) {
|
|
645
|
+
const profileManifest = JSON.parse(readFileSync(profileManifestPath, 'utf8'));
|
|
646
|
+
const editionId = profileManifest.dsh?.profile?.plusEdition;
|
|
647
|
+
if (editionId !== undefined) {
|
|
648
|
+
const distribution = readDistributionProfile(paths.distributionDirectory);
|
|
649
|
+
const edition = distribution.editions[editionId];
|
|
650
|
+
if (edition === undefined) {
|
|
651
|
+
check(false, 'edition "' + editionId + '" is not declared by this distribution');
|
|
652
|
+
}
|
|
653
|
+
else {
|
|
654
|
+
const bundles = new Set(profileManifest.dsh?.profile?.bundles ?? []);
|
|
655
|
+
const missingBundles = edition.bundles.filter(bundle => !bundles.has(bundle));
|
|
656
|
+
const packageNames = Object.keys(edition.packages);
|
|
657
|
+
const missingPackages = packageNames.filter(name => !existsSync(join(paths.profileDirectory, 'node_modules', ...name.split('/'), 'package.json')));
|
|
658
|
+
check(missingBundles.length === 0, 'edition ' + editionId + ': ' + String(edition.bundles.length - missingBundles.length) + '/'
|
|
659
|
+
+ String(edition.bundles.length) + ' bundles mounted'
|
|
660
|
+
+ (missingBundles.length === 0 ? '' : ' (missing ' + missingBundles.join(', ') + ')'));
|
|
661
|
+
check(missingPackages.length === 0, 'edition ' + editionId + ': ' + String(packageNames.length - missingPackages.length) + '/'
|
|
662
|
+
+ String(packageNames.length) + ' packages installed'
|
|
663
|
+
+ (missingPackages.length === 0 ? '' : ' (missing ' + missingPackages.join(', ') + ')'));
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
584
667
|
console.log(failures === 0 ? 'No problems found.' : String(failures) + ' problem(s) found.');
|
|
585
668
|
return failures === 0 ? 0 : 1;
|
|
586
669
|
}
|
|
@@ -588,6 +671,7 @@ const USAGE = [
|
|
|
588
671
|
'Usage: dsh-plus <command> [options]',
|
|
589
672
|
'',
|
|
590
673
|
'Commands:',
|
|
674
|
+
' install install an edition package manifest into the profile',
|
|
591
675
|
' start start the server (first run creates the profile)',
|
|
592
676
|
' stop stop the server',
|
|
593
677
|
' status report whether the server is running',
|
|
@@ -600,6 +684,11 @@ const USAGE = [
|
|
|
600
684
|
' --host <h> interface to bind (default 127.0.0.1)',
|
|
601
685
|
' --no-open do not open a browser',
|
|
602
686
|
' --foreground run in this terminal instead of in the background',
|
|
687
|
+
' --edition <id> install the named edition manifest (personal or team)',
|
|
688
|
+
'',
|
|
689
|
+
'install options:',
|
|
690
|
+
' --edition <id> package manifest to install (personal or team)',
|
|
691
|
+
' --profile <n> profile name to install into (default: this installation)',
|
|
603
692
|
'',
|
|
604
693
|
'capabilities options:',
|
|
605
694
|
' --enable <ids> enable the comma-separated capability ids',
|
|
@@ -622,6 +711,8 @@ export async function runStandaloneCli(argv) {
|
|
|
622
711
|
console.log(USAGE);
|
|
623
712
|
return 0;
|
|
624
713
|
}
|
|
714
|
+
if (command === 'install')
|
|
715
|
+
return install(rest);
|
|
625
716
|
if (command === 'start')
|
|
626
717
|
return await start(rest);
|
|
627
718
|
if (command === 'stop')
|
|
@@ -93,12 +93,18 @@ export declare function isWithin(candidate: string, parent: string, platform?: N
|
|
|
93
93
|
*/
|
|
94
94
|
export declare function resolveDistributionDirectory(anchor: string): string;
|
|
95
95
|
/** The reviewed bundle order and pins the installed distribution declares. */
|
|
96
|
+
export interface EditionComposition {
|
|
97
|
+
readonly label: string;
|
|
98
|
+
readonly bundles: readonly string[];
|
|
99
|
+
readonly packages: Readonly<Record<string, string>>;
|
|
100
|
+
}
|
|
96
101
|
export declare function readDistributionProfile(distributionDirectory: string): {
|
|
97
102
|
readonly name: string;
|
|
98
103
|
readonly bundles: readonly string[];
|
|
99
104
|
readonly dependencies: Readonly<Record<string, string>>;
|
|
100
105
|
readonly allowBuilds: Readonly<Record<string, boolean>>;
|
|
101
106
|
readonly overrides: Readonly<Record<string, string>>;
|
|
107
|
+
readonly editions: Readonly<Record<string, EditionComposition>>;
|
|
102
108
|
readonly dshRange: string;
|
|
103
109
|
readonly version: string;
|
|
104
110
|
};
|
|
@@ -191,9 +197,10 @@ export declare function resolvePaths(anchor: string, env?: NodeJS.ProcessEnv): S
|
|
|
191
197
|
*
|
|
192
198
|
* @param paths - resolved standalone paths.
|
|
193
199
|
* @param consumerDirectory - directory whose `node_modules` holds the installed packages.
|
|
200
|
+
* @param edition - edition id whose package list to install, or undefined for the default.
|
|
194
201
|
* @returns whether this call created the manifest.
|
|
195
202
|
*/
|
|
196
|
-
export declare function ensureProfile(paths: StandalonePaths, consumerDirectory: string): boolean;
|
|
203
|
+
export declare function ensureProfile(paths: StandalonePaths, consumerDirectory: string, edition?: string): boolean;
|
|
197
204
|
/**
|
|
198
205
|
* Apply the reviewed npm-target patches to the profile's installed packages.
|
|
199
206
|
*
|
|
@@ -11,9 +11,11 @@
|
|
|
11
11
|
import { spawnSync } from 'node:child_process';
|
|
12
12
|
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
13
13
|
import { createRequire } from 'node:module';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
14
15
|
import { homedir } from 'node:os';
|
|
15
16
|
import { dirname, join, posix, resolve, win32 } from 'node:path';
|
|
16
17
|
import { parseDocument } from 'yaml';
|
|
18
|
+
import { readPatchSet } from "./patch-set.js";
|
|
17
19
|
/** Profile name a standalone installation owns when its package declares none. */
|
|
18
20
|
export const STANDALONE_PROFILE = 'plus';
|
|
19
21
|
/**
|
|
@@ -97,6 +99,12 @@ function requireStringArray(value, label) {
|
|
|
97
99
|
throw new Error(label + ' must be a string array');
|
|
98
100
|
return value;
|
|
99
101
|
}
|
|
102
|
+
/** Read one non-empty string field. */
|
|
103
|
+
function requireString(value, label) {
|
|
104
|
+
if (typeof value !== 'string' || value.trim() === '')
|
|
105
|
+
throw new Error(label + ' must be a non-empty string');
|
|
106
|
+
return value;
|
|
107
|
+
}
|
|
100
108
|
/** Resolve the DSH home, honouring the environment override the launcher itself reads. */
|
|
101
109
|
export function resolveHome(env = process.env) {
|
|
102
110
|
const configured = env.DSH_HOME;
|
|
@@ -135,12 +143,15 @@ export function isWithin(candidate, parent, platform = process.platform) {
|
|
|
135
143
|
* @returns absolute path to the installed distribution.
|
|
136
144
|
*/
|
|
137
145
|
export function resolveDistributionDirectory(anchor) {
|
|
146
|
+
const ownDirectory = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
138
147
|
let resolved;
|
|
139
148
|
try {
|
|
140
149
|
resolved = dirname(createRequire(anchor).resolve('@sparkelf/dsh-plus/package.json'));
|
|
141
150
|
}
|
|
142
151
|
catch {
|
|
143
|
-
|
|
152
|
+
// `npx @sparkelf/dsh-plus ...` runs the CLI from a temporary install, so the consumer
|
|
153
|
+
// tree has no copy to resolve. The running module's own package is the distribution.
|
|
154
|
+
return ownDirectory;
|
|
144
155
|
}
|
|
145
156
|
// A resolution that leaves the consumer tree means the module answered from its own
|
|
146
157
|
// installation, which reports health for a distribution the consumer never installed.
|
|
@@ -156,21 +167,14 @@ export function resolveDistributionDirectory(anchor) {
|
|
|
156
167
|
break;
|
|
157
168
|
current = parent;
|
|
158
169
|
}
|
|
159
|
-
|
|
170
|
+
return ownDirectory;
|
|
160
171
|
}
|
|
161
|
-
/** The reviewed bundle order and pins the installed distribution declares. */
|
|
162
172
|
export function readDistributionProfile(distributionDirectory) {
|
|
163
173
|
const manifest = requireRecord(JSON.parse(readFileSync(join(distributionDirectory, 'package.json'), 'utf8')), 'Plus distribution manifest');
|
|
164
174
|
const plus = requireRecord(manifest.dshPlus, 'dshPlus');
|
|
165
175
|
const profile = requireRecord(plus.profile, 'dshPlus.profile');
|
|
166
|
-
const
|
|
176
|
+
const dependencies = requireDependencyMap(requireRecord(profile.dependencies, 'dshPlus.profile.dependencies'), 'dshPlus.profile.dependencies');
|
|
167
177
|
const rawAllowBuilds = requireRecord(profile.allowBuilds, 'dshPlus.profile.allowBuilds');
|
|
168
|
-
const dependencies = {};
|
|
169
|
-
for (const [name, spec] of Object.entries(rawDependencies)) {
|
|
170
|
-
if (typeof spec !== 'string' || spec === '')
|
|
171
|
-
throw new Error('dshPlus.profile.dependencies.' + name + ' must be a non-empty string');
|
|
172
|
-
dependencies[name] = spec;
|
|
173
|
-
}
|
|
174
178
|
const allowBuilds = {};
|
|
175
179
|
for (const [name, allowed] of Object.entries(rawAllowBuilds)) {
|
|
176
180
|
if (typeof allowed !== 'boolean')
|
|
@@ -189,6 +193,16 @@ export function readDistributionProfile(distributionDirectory) {
|
|
|
189
193
|
throw new Error('dshPlus.profile.overrides.' + name + ' must be a non-empty string');
|
|
190
194
|
overrides[name] = spec;
|
|
191
195
|
}
|
|
196
|
+
const editions = {};
|
|
197
|
+
const rawEditions = plus.editions === undefined ? {} : requireRecord(plus.editions, 'dshPlus.editions');
|
|
198
|
+
for (const [id, value] of Object.entries(rawEditions)) {
|
|
199
|
+
const edition = requireRecord(value, 'dshPlus.editions.' + id);
|
|
200
|
+
editions[id] = {
|
|
201
|
+
label: requireString(edition.label, 'dshPlus.editions.' + id + '.label'),
|
|
202
|
+
bundles: requireStringArray(edition.bundles, 'dshPlus.editions.' + id + '.bundles'),
|
|
203
|
+
packages: requireDependencyMap(requireRecord(edition.packages, 'dshPlus.editions.' + id + '.packages'), 'dshPlus.editions.' + id + '.packages'),
|
|
204
|
+
};
|
|
205
|
+
}
|
|
192
206
|
const compatibility = requireRecord(plus.compatibility, 'dshPlus.compatibility');
|
|
193
207
|
return {
|
|
194
208
|
name: String(manifest.name),
|
|
@@ -196,10 +210,21 @@ export function readDistributionProfile(distributionDirectory) {
|
|
|
196
210
|
dependencies,
|
|
197
211
|
allowBuilds,
|
|
198
212
|
overrides,
|
|
213
|
+
editions,
|
|
199
214
|
dshRange: String(compatibility.dsh),
|
|
200
215
|
version: String(manifest.version),
|
|
201
216
|
};
|
|
202
217
|
}
|
|
218
|
+
/** Read one package-name-to-spec map, rejecting empty entries. */
|
|
219
|
+
function requireDependencyMap(value, label) {
|
|
220
|
+
const dependencies = {};
|
|
221
|
+
for (const [name, spec] of Object.entries(value)) {
|
|
222
|
+
if (typeof spec !== 'string' || spec === '')
|
|
223
|
+
throw new Error(label + '.' + name + ' must be a non-empty string');
|
|
224
|
+
dependencies[name] = spec;
|
|
225
|
+
}
|
|
226
|
+
return dependencies;
|
|
227
|
+
}
|
|
203
228
|
/**
|
|
204
229
|
* Give the profile its own installed tree, built from the distribution's declarations.
|
|
205
230
|
*
|
|
@@ -221,7 +246,12 @@ export function readDistributionProfile(distributionDirectory) {
|
|
|
221
246
|
export function installProfilePackages(paths, consumerDirectory) {
|
|
222
247
|
const consumerModules = join(consumerDirectory, 'node_modules');
|
|
223
248
|
if (!existsSync(consumerModules)) {
|
|
224
|
-
|
|
249
|
+
// `npx @sparkelf/dsh-plus` runs from a temporary package tree that has no launcher
|
|
250
|
+
// beside it. The profile installs its own tree, so there is nothing to link from
|
|
251
|
+
// the caller; only the profile's packages matter.
|
|
252
|
+
installWithRegistryFallback(paths);
|
|
253
|
+
alignReplacedPackageNames(join(paths.profileDirectory, 'node_modules'));
|
|
254
|
+
return;
|
|
225
255
|
}
|
|
226
256
|
const profileModules = join(paths.profileDirectory, 'node_modules');
|
|
227
257
|
if (existsSync(profileModules)) {
|
|
@@ -250,6 +280,9 @@ function installWithRegistryFallback(paths) {
|
|
|
250
280
|
cwd: paths.profileDirectory,
|
|
251
281
|
stdio: 'inherit',
|
|
252
282
|
shell: process.platform === 'win32',
|
|
283
|
+
// A registry that accepts the connection but never answers would otherwise hold the
|
|
284
|
+
// first start for pnpm's full retry budget before the fallback got a chance.
|
|
285
|
+
timeout: 5 * 60_000,
|
|
253
286
|
});
|
|
254
287
|
if (result.error !== undefined)
|
|
255
288
|
throw result.error;
|
|
@@ -430,12 +463,61 @@ export function resolvePaths(anchor, env = process.env) {
|
|
|
430
463
|
*
|
|
431
464
|
* @param paths - resolved standalone paths.
|
|
432
465
|
* @param consumerDirectory - directory whose `node_modules` holds the installed packages.
|
|
466
|
+
* @param edition - edition id whose package list to install, or undefined for the default.
|
|
433
467
|
* @returns whether this call created the manifest.
|
|
434
468
|
*/
|
|
435
|
-
export function ensureProfile(paths, consumerDirectory) {
|
|
469
|
+
export function ensureProfile(paths, consumerDirectory, edition) {
|
|
436
470
|
const manifestPath = join(paths.profileDirectory, 'package.json');
|
|
437
471
|
const distribution = readDistributionProfile(paths.distributionDirectory);
|
|
472
|
+
const composition = edition === undefined ? undefined : distribution.editions[edition];
|
|
473
|
+
if (edition !== undefined && composition === undefined) {
|
|
474
|
+
const offered = Object.keys(distribution.editions);
|
|
475
|
+
throw new Error('unknown edition "' + edition + '"; this release offers '
|
|
476
|
+
+ (offered.length === 0 ? '(none)' : offered.join(', ')));
|
|
477
|
+
}
|
|
478
|
+
// An edition lists only third-party, fork, and self-developed plugins. The official
|
|
479
|
+
// runtime is the CLI's responsibility and must not be duplicated in that list.
|
|
480
|
+
for (const name of Object.keys(composition?.packages ?? {})) {
|
|
481
|
+
if (name.startsWith('@deepseek-ai/')) {
|
|
482
|
+
throw new Error('edition "' + String(edition) + '" lists the official package ' + name
|
|
483
|
+
+ '; editions contain only third-party or self-developed plugins');
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
const runtime = distribution.dshRange.replace(/^>=?/u, '');
|
|
487
|
+
const baseBundles = composition === undefined
|
|
488
|
+
? distribution.bundles
|
|
489
|
+
: distribution.bundles.filter(bundle => bundle.startsWith('@deepseek-ai/') || bundle === '@sparkelf/dsh-plus');
|
|
490
|
+
const basePackages = composition === undefined
|
|
491
|
+
? distribution.dependencies
|
|
492
|
+
: {
|
|
493
|
+
'@deepseek-ai/dsh': distribution.dshRange,
|
|
494
|
+
'@sparkelf/dsh-plus': distribution.version,
|
|
495
|
+
...Object.fromEntries(baseBundles
|
|
496
|
+
.filter(bundle => bundle.startsWith('@deepseek-ai/dsh-'))
|
|
497
|
+
.map(bundle => [bundle, runtime])),
|
|
498
|
+
};
|
|
499
|
+
const compositionPackages = composition === undefined
|
|
500
|
+
? basePackages
|
|
501
|
+
: { ...basePackages, ...composition.packages };
|
|
502
|
+
const compositionBundles = composition === undefined
|
|
503
|
+
? baseBundles
|
|
504
|
+
: [...baseBundles, ...composition.bundles];
|
|
438
505
|
if (existsSync(manifestPath)) {
|
|
506
|
+
// A selected edition is an explicit install choice, so its packages and bundle order
|
|
507
|
+
// replace the profile's rather than being ignored. Other profile decisions stay.
|
|
508
|
+
if (composition !== undefined) {
|
|
509
|
+
const manifest = requireRecord(JSON.parse(readFileSync(manifestPath, 'utf8')), 'Plus profile manifest');
|
|
510
|
+
manifest.dependencies = { ...manifest.dependencies, ...compositionPackages };
|
|
511
|
+
manifest.dsh = {
|
|
512
|
+
...manifest.dsh,
|
|
513
|
+
profile: {
|
|
514
|
+
...manifest.dsh?.profile,
|
|
515
|
+
bundles: [...compositionBundles],
|
|
516
|
+
...(edition === undefined ? {} : { plusEdition: edition }),
|
|
517
|
+
},
|
|
518
|
+
};
|
|
519
|
+
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
|
|
520
|
+
}
|
|
439
521
|
// The workspace carries decisions the distribution owns — overrides and the build
|
|
440
522
|
// script allowlist — and a distribution release changes them. Rewriting on every
|
|
441
523
|
// start is what lets an upgraded installation receive the new values; a profile
|
|
@@ -449,18 +531,12 @@ export function ensureProfile(paths, consumerDirectory) {
|
|
|
449
531
|
name: 'dsh-profile-' + STANDALONE_PROFILE,
|
|
450
532
|
private: true,
|
|
451
533
|
type: 'module',
|
|
452
|
-
dependencies:
|
|
453
|
-
'@sparkelf/dsh-plus': distribution.version,
|
|
454
|
-
// The launcher's own tree supplies every service package the bundles mount. A
|
|
455
|
-
// profile that lists only the distribution's plugins installs a partial tree and
|
|
456
|
-
// fails at load with a module the launcher would have carried.
|
|
457
|
-
'@deepseek-ai/dsh': distribution.dshRange,
|
|
458
|
-
...distribution.dependencies,
|
|
459
|
-
},
|
|
534
|
+
dependencies: compositionPackages,
|
|
460
535
|
dsh: {
|
|
461
536
|
profile: {
|
|
462
|
-
bundles:
|
|
537
|
+
bundles: [...compositionBundles],
|
|
463
538
|
patchReload: 'live',
|
|
539
|
+
...(edition === undefined ? {} : { plusEdition: edition }),
|
|
464
540
|
},
|
|
465
541
|
},
|
|
466
542
|
};
|
|
@@ -536,34 +612,20 @@ function git(root, args, acceptFailure = false) {
|
|
|
536
612
|
*/
|
|
537
613
|
export function applyProfileNpmPatches(distributionDirectory, profileDirectory) {
|
|
538
614
|
const manifest = requireRecord(JSON.parse(readFileSync(join(distributionDirectory, 'package.json'), 'utf8')), 'Plus distribution manifest');
|
|
539
|
-
const
|
|
540
|
-
const names = plus.patchPackages;
|
|
541
|
-
if (!Array.isArray(names))
|
|
542
|
-
throw new Error('dshPlus.patchPackages must be an array');
|
|
615
|
+
const patchSet = readPatchSet(distributionDirectory, manifest);
|
|
543
616
|
const applied = [];
|
|
544
|
-
for (const
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
const
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
// official checkout, which a standalone installation does not have.
|
|
557
|
-
if (target.kind !== 'npm')
|
|
558
|
-
continue;
|
|
559
|
-
const targetName = String(target.name);
|
|
560
|
-
const patched = resolveInstalledPackage(profileDirectory, targetName);
|
|
561
|
-
const file = resolve(patchPackage.directory, String(variant.file));
|
|
562
|
-
if (git(patched.directory, ['apply', '--reverse', '--check', file], true) !== undefined)
|
|
563
|
-
continue;
|
|
564
|
-
git(patched.directory, ['apply', file]);
|
|
565
|
-
applied.push(value + ' -> ' + targetName);
|
|
566
|
-
}
|
|
617
|
+
for (const entry of [...patchSet.entries, ...patchSet.thirdPartyPatches]) {
|
|
618
|
+
// Only npm targets reach an installed package; a source target needs the
|
|
619
|
+
// official checkout, which a standalone installation does not have.
|
|
620
|
+
if (entry.target.kind !== 'npm')
|
|
621
|
+
continue;
|
|
622
|
+
const targetName = entry.target.name;
|
|
623
|
+
const patched = resolveInstalledPackage(profileDirectory, targetName);
|
|
624
|
+
const file = resolve(distributionDirectory, entry.file);
|
|
625
|
+
if (git(patched.directory, ['apply', '--reverse', '--check', file], true) !== undefined)
|
|
626
|
+
continue;
|
|
627
|
+
git(patched.directory, ['apply', file]);
|
|
628
|
+
applied.push(entry.id + ' -> ' + targetName);
|
|
567
629
|
}
|
|
568
630
|
return applied;
|
|
569
631
|
}
|
|
@@ -575,7 +637,22 @@ function resolveInstalledPackage(from, packageName) {
|
|
|
575
637
|
manifestPath = requireFrom.resolve(packageName + '/package.json');
|
|
576
638
|
}
|
|
577
639
|
catch {
|
|
578
|
-
|
|
640
|
+
// Some packages do not export `./package.json`; resolve the entry module instead and walk
|
|
641
|
+
// up to the directory that holds the manifest.
|
|
642
|
+
let directory;
|
|
643
|
+
try {
|
|
644
|
+
directory = dirname(requireFrom.resolve(packageName));
|
|
645
|
+
}
|
|
646
|
+
catch {
|
|
647
|
+
throw new Error(packageName + ' is not installed under ' + from);
|
|
648
|
+
}
|
|
649
|
+
while (!existsSync(resolve(directory, 'package.json'))) {
|
|
650
|
+
const parent = dirname(directory);
|
|
651
|
+
if (parent === directory)
|
|
652
|
+
throw new Error(packageName + ' has no resolvable package manifest under ' + from);
|
|
653
|
+
directory = parent;
|
|
654
|
+
}
|
|
655
|
+
manifestPath = resolve(directory, 'package.json');
|
|
579
656
|
}
|
|
580
657
|
const manifest = requireRecord(JSON.parse(readFileSync(manifestPath, 'utf8')), packageName + ' manifest');
|
|
581
658
|
return { name: packageName, version: String(manifest.version), directory: dirname(manifestPath), manifest };
|