@sparkelf/dsh-plus 0.1.0-rc.9 → 0.2.0-rc.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.i18n.yaml +2 -2
- package/README.md +7 -4
- package/README.zh.md +7 -4
- package/cordis.patch.yml +17 -14
- package/lib/apply-e0aMC4B4.js +601 -0
- package/lib/apply.js +2 -374
- package/lib/bin.js +1030 -3
- package/lib/types/apply.d.ts +19 -0
- package/lib/types/apply.js +642 -0
- package/lib/types/bin.js +32 -0
- package/lib/types/index.js +9 -0
- package/lib/types/registry-versions.d.ts +31 -0
- package/lib/types/registry-versions.js +61 -0
- package/lib/types/standalone-cli.d.ts +17 -0
- package/lib/types/standalone-cli.js +359 -0
- package/lib/types/standalone-profile.d.ts +170 -0
- package/lib/types/standalone-profile.js +504 -0
- package/lib/types/standalone-server.d.ts +70 -0
- package/lib/types/standalone-server.js +158 -0
- package/package.json +142 -84
- package/lib/invariant.js +0 -19
- package/lib/types/invariant.d.ts +0 -16
|
@@ -0,0 +1,601 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { chmodSync, copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { dirname, isAbsolute, relative, resolve } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { satisfies, valid, validRange } from "semver";
|
|
7
|
+
import { parseDocument } from "yaml";
|
|
8
|
+
//#region lib/types/apply.js
|
|
9
|
+
/** Materialize the Plus distribution's independent patch packages without Desktop. */
|
|
10
|
+
const FORMAT_VERSION = 1;
|
|
11
|
+
function isSelectedNpmPatch(patch) {
|
|
12
|
+
return patch.target.kind === "npm";
|
|
13
|
+
}
|
|
14
|
+
function readJsonObject(path, label) {
|
|
15
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
16
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(label + " must be a JSON object");
|
|
17
|
+
return parsed;
|
|
18
|
+
}
|
|
19
|
+
function requireObject(value, label) {
|
|
20
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error(label + " must be an object");
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
function requireString(value, label) {
|
|
24
|
+
if (typeof value !== "string" || value === "") throw new Error(label + " must be a non-empty string");
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
function requireRange(value, label) {
|
|
28
|
+
const range = requireString(value, label);
|
|
29
|
+
if (validRange(range) === null) throw new Error(label + " is not a semantic-version range: " + range);
|
|
30
|
+
return range;
|
|
31
|
+
}
|
|
32
|
+
function requireVersion(value, label) {
|
|
33
|
+
const version = requireString(value, label);
|
|
34
|
+
if (valid(version) === null) throw new Error(label + " is not a semantic version: " + version);
|
|
35
|
+
return version;
|
|
36
|
+
}
|
|
37
|
+
function requireStringArray(value, label) {
|
|
38
|
+
if (!Array.isArray(value) || value.length === 0) throw new Error(label + " must be a non-empty array");
|
|
39
|
+
const strings = value.map((entry, index) => requireString(entry, label + "[" + String(index) + "]"));
|
|
40
|
+
if (new Set(strings).size !== strings.length) throw new Error(label + " must not contain duplicates");
|
|
41
|
+
return strings;
|
|
42
|
+
}
|
|
43
|
+
function resolveInstalledPackage(distributionDirectory, packageName) {
|
|
44
|
+
const manifestPath = createRequire(resolve(distributionDirectory, "package.json")).resolve(packageName + "/package.json");
|
|
45
|
+
const manifest = readJsonObject(manifestPath, packageName + " manifest");
|
|
46
|
+
return {
|
|
47
|
+
name: packageName,
|
|
48
|
+
version: requireVersion(manifest.version, packageName + " version"),
|
|
49
|
+
directory: dirname(manifestPath),
|
|
50
|
+
manifest
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function resolvePatchFile(packageDirectory, declared) {
|
|
54
|
+
const absoluteFile = resolve(packageDirectory, declared);
|
|
55
|
+
const local = relative(packageDirectory, absoluteFile);
|
|
56
|
+
if (isAbsolute(local) || local === ".." || local.startsWith("../")) throw new Error("patch file must stay inside its patch package: " + declared);
|
|
57
|
+
if (!existsSync(absoluteFile)) throw new Error("patch file does not exist: " + absoluteFile);
|
|
58
|
+
return {
|
|
59
|
+
relativeFile: local.replaceAll("\\", "/"),
|
|
60
|
+
absoluteFile
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function parseVariant(value, index, patchPackage) {
|
|
64
|
+
const label = patchPackage.name + " dshPatch.variants[" + String(index) + "]";
|
|
65
|
+
const variant = requireObject(value, label);
|
|
66
|
+
const target = requireObject(variant.target, label + ".target");
|
|
67
|
+
const kind = requireString(target.kind, label + ".target.kind");
|
|
68
|
+
const common = {
|
|
69
|
+
id: requireString(variant.id, label + ".id"),
|
|
70
|
+
dsh: requireRange(variant.dsh, label + ".dsh"),
|
|
71
|
+
file: requireString(variant.file, label + ".file"),
|
|
72
|
+
patchPackage
|
|
73
|
+
};
|
|
74
|
+
if (kind === "npm") return {
|
|
75
|
+
...common,
|
|
76
|
+
target: {
|
|
77
|
+
kind,
|
|
78
|
+
name: requireString(target.name, label + ".target.name"),
|
|
79
|
+
range: requireRange(target.range, label + ".target.range")
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
if (kind === "dsh-source") return {
|
|
83
|
+
...common,
|
|
84
|
+
target: {
|
|
85
|
+
kind,
|
|
86
|
+
baseRevision: requireString(target.baseRevision, label + ".target.baseRevision")
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
throw new Error("unsupported patch target kind: " + kind);
|
|
90
|
+
}
|
|
91
|
+
function git(root, args, acceptFailure = false) {
|
|
92
|
+
const result = spawnSync("git", args, {
|
|
93
|
+
cwd: root,
|
|
94
|
+
encoding: "utf8"
|
|
95
|
+
});
|
|
96
|
+
if (result.status === 0) return result.stdout.trim();
|
|
97
|
+
if (acceptFailure) return void 0;
|
|
98
|
+
const detail = result.stderr.trim();
|
|
99
|
+
throw new Error("git " + args.join(" ") + " failed" + (detail === "" ? "" : ": " + detail));
|
|
100
|
+
}
|
|
101
|
+
function sourcePatchState(root, file) {
|
|
102
|
+
if (git(root, [
|
|
103
|
+
"apply",
|
|
104
|
+
"--check",
|
|
105
|
+
file
|
|
106
|
+
], true) !== void 0) return "pending";
|
|
107
|
+
if (git(root, [
|
|
108
|
+
"apply",
|
|
109
|
+
"--reverse",
|
|
110
|
+
"--check",
|
|
111
|
+
file
|
|
112
|
+
], true) !== void 0) return "applied";
|
|
113
|
+
git(root, [
|
|
114
|
+
"apply",
|
|
115
|
+
"--check",
|
|
116
|
+
file
|
|
117
|
+
]);
|
|
118
|
+
throw new Error("unreachable");
|
|
119
|
+
}
|
|
120
|
+
/** 在隔离index中按真实顺序预演source patches,避免失败后污染official worktree。 */
|
|
121
|
+
function preflightSourcePatches(root, files) {
|
|
122
|
+
if (files.length === 0) return;
|
|
123
|
+
const directory = mkdtempSync(resolve(tmpdir(), "dsh-plus-source-patches-"));
|
|
124
|
+
const index = resolve(directory, "index");
|
|
125
|
+
const env = {
|
|
126
|
+
...process.env,
|
|
127
|
+
GIT_INDEX_FILE: index
|
|
128
|
+
};
|
|
129
|
+
const run = (args) => {
|
|
130
|
+
const result = spawnSync("git", args, {
|
|
131
|
+
cwd: root,
|
|
132
|
+
encoding: "utf8",
|
|
133
|
+
env
|
|
134
|
+
});
|
|
135
|
+
if (result.status === 0) return;
|
|
136
|
+
const detail = result.stderr.trim();
|
|
137
|
+
throw new Error("git " + args.join(" ") + " failed" + (detail === "" ? "" : ": " + detail));
|
|
138
|
+
};
|
|
139
|
+
try {
|
|
140
|
+
run(["read-tree", "HEAD"]);
|
|
141
|
+
for (const file of files) run([
|
|
142
|
+
"apply",
|
|
143
|
+
"--cached",
|
|
144
|
+
"--3way",
|
|
145
|
+
file
|
|
146
|
+
]);
|
|
147
|
+
} finally {
|
|
148
|
+
rmSync(directory, {
|
|
149
|
+
recursive: true,
|
|
150
|
+
force: true
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function requireSourceBase(root, baseRevision) {
|
|
155
|
+
const checkoutRevision = requireString(git(root, ["rev-parse", "HEAD"]), "DSH checkout revision");
|
|
156
|
+
if (checkoutRevision !== baseRevision) throw new Error("DSH checkout must be exact official base " + baseRevision + ", found " + checkoutRevision);
|
|
157
|
+
return checkoutRevision;
|
|
158
|
+
}
|
|
159
|
+
function patchPackageVariants(distributionDirectory, names) {
|
|
160
|
+
const variants = [];
|
|
161
|
+
for (const name of names) {
|
|
162
|
+
const patchPackage = resolveInstalledPackage(distributionDirectory, name);
|
|
163
|
+
const declaration = requireObject(patchPackage.manifest.dshPatch, name + " dshPatch");
|
|
164
|
+
if (declaration.formatVersion !== FORMAT_VERSION) throw new Error("unsupported dshPatch formatVersion in " + name + ": " + String(declaration.formatVersion));
|
|
165
|
+
if (!Array.isArray(declaration.variants) || declaration.variants.length === 0) throw new Error(name + " dshPatch.variants must be a non-empty array");
|
|
166
|
+
for (const [index, value] of declaration.variants.entries()) variants.push(parseVariant(value, index, patchPackage));
|
|
167
|
+
}
|
|
168
|
+
return variants;
|
|
169
|
+
}
|
|
170
|
+
function prepareSelection(distributionDirectory, profileDirectory, dshRoot) {
|
|
171
|
+
const distributionManifest = readJsonObject(resolve(distributionDirectory, "package.json"), "Plus distribution manifest");
|
|
172
|
+
const distributionName = requireString(distributionManifest.name, "Plus distribution name");
|
|
173
|
+
const distributionVersion = requireVersion(distributionManifest.version, "Plus distribution version");
|
|
174
|
+
const plus = requireObject(distributionManifest.dshPlus, "Plus distribution dshPlus");
|
|
175
|
+
if (plus.formatVersion !== FORMAT_VERSION) throw new Error("unsupported dshPlus formatVersion: " + String(plus.formatVersion));
|
|
176
|
+
const dshRange = requireRange(requireObject(plus.compatibility, "dshPlus.compatibility").dsh, "dshPlus.compatibility.dsh");
|
|
177
|
+
const baseRevision = requireString(requireObject(plus.sourceBase, "dshPlus.sourceBase").revision, "dshPlus.sourceBase.revision");
|
|
178
|
+
const profile = requireObject(plus.profile, "dshPlus.profile");
|
|
179
|
+
const bundles = requireStringArray(profile.bundles, "dshPlus.profile.bundles");
|
|
180
|
+
const rawProfileDependencySpecs = requireObject(profile.dependencies, "dshPlus.profile.dependencies");
|
|
181
|
+
const profileDependencySpecs = {};
|
|
182
|
+
for (const [name, spec] of Object.entries(rawProfileDependencySpecs)) profileDependencySpecs[name] = requireString(spec, "dshPlus.profile.dependencies." + name);
|
|
183
|
+
const rawAllowBuilds = requireObject(profile.allowBuilds, "dshPlus.profile.allowBuilds");
|
|
184
|
+
const allowBuilds = {};
|
|
185
|
+
for (const [name, allowed] of Object.entries(rawAllowBuilds)) {
|
|
186
|
+
if (typeof allowed !== "boolean") throw new Error("dshPlus.profile.allowBuilds." + name + " must be a boolean");
|
|
187
|
+
allowBuilds[name] = allowed;
|
|
188
|
+
}
|
|
189
|
+
const patchPackageNames = requireStringArray(plus.patchPackages, "dshPlus.patchPackages");
|
|
190
|
+
const dependencies = requireObject(distributionManifest.dependencies, "Plus distribution dependencies");
|
|
191
|
+
const dshVersion = requireVersion(readJsonObject(resolve(dshRoot, "package.json"), "DSH source manifest").version, "DSH source version");
|
|
192
|
+
if (!satisfies(dshVersion, dshRange, { includePrerelease: true })) throw new Error(distributionName + "@" + distributionVersion + " requires DSH " + dshRange + ", found " + dshVersion);
|
|
193
|
+
const checkoutRevision = requireSourceBase(dshRoot, baseRevision);
|
|
194
|
+
const groups = /* @__PURE__ */ new Map();
|
|
195
|
+
for (const variant of patchPackageVariants(distributionDirectory, patchPackageNames)) {
|
|
196
|
+
const entries = groups.get(variant.id) ?? [];
|
|
197
|
+
entries.push(variant);
|
|
198
|
+
groups.set(variant.id, entries);
|
|
199
|
+
}
|
|
200
|
+
const sourceStates = /* @__PURE__ */ new Map();
|
|
201
|
+
const orderedGroups = [...groups].sort(([left], [right]) => left.localeCompare(right));
|
|
202
|
+
for (const [, variants] of orderedGroups) for (const variant of variants) {
|
|
203
|
+
if (variant.target.kind !== "dsh-source" || variant.target.baseRevision !== baseRevision || !satisfies(dshVersion, variant.dsh, { includePrerelease: true })) continue;
|
|
204
|
+
const file = resolvePatchFile(variant.patchPackage.directory, variant.file);
|
|
205
|
+
sourceStates.set(file.absoluteFile, sourcePatchState(dshRoot, file.absoluteFile));
|
|
206
|
+
}
|
|
207
|
+
preflightSourcePatches(dshRoot, [...sourceStates].flatMap(([file, state]) => state === "pending" ? [file] : []));
|
|
208
|
+
writeProfileRequirements(profileDirectory, profileDependencySpecs, allowBuilds);
|
|
209
|
+
runPnpm(profileDirectory, ["install", "--no-frozen-lockfile"], "pnpm install Plus profile dependencies");
|
|
210
|
+
const profileDependencies = Object.keys(profileDependencySpecs).sort().map((name) => {
|
|
211
|
+
return {
|
|
212
|
+
name,
|
|
213
|
+
version: resolveInstalledPackage(profileDirectory, name).version
|
|
214
|
+
};
|
|
215
|
+
});
|
|
216
|
+
const runtimePackages = [...Object.keys(dependencies).filter((name) => !patchPackageNames.includes(name)).sort().map((name) => {
|
|
217
|
+
return {
|
|
218
|
+
name,
|
|
219
|
+
version: resolveInstalledPackage(distributionDirectory, name).version
|
|
220
|
+
};
|
|
221
|
+
}), ...profileDependencies].sort((left, right) => left.name.localeCompare(right.name));
|
|
222
|
+
const selected = [];
|
|
223
|
+
for (const [id, variants] of orderedGroups) {
|
|
224
|
+
const candidates = variants.flatMap((variant) => {
|
|
225
|
+
if (!satisfies(dshVersion, variant.dsh, { includePrerelease: true })) return [];
|
|
226
|
+
const file = resolvePatchFile(variant.patchPackage.directory, variant.file);
|
|
227
|
+
if (variant.target.kind === "dsh-source") {
|
|
228
|
+
if (variant.target.baseRevision !== baseRevision) return [];
|
|
229
|
+
return [{
|
|
230
|
+
id,
|
|
231
|
+
patchPackage: {
|
|
232
|
+
name: variant.patchPackage.name,
|
|
233
|
+
version: variant.patchPackage.version
|
|
234
|
+
},
|
|
235
|
+
file: file.relativeFile,
|
|
236
|
+
absoluteFile: file.absoluteFile,
|
|
237
|
+
target: {
|
|
238
|
+
kind: "dsh-source",
|
|
239
|
+
baseRevision
|
|
240
|
+
},
|
|
241
|
+
alreadyApplied: sourceStates.get(file.absoluteFile) === "applied"
|
|
242
|
+
}];
|
|
243
|
+
}
|
|
244
|
+
const targetVersion = resolveInstalledPackage(variant.target.name in profileDependencySpecs ? profileDirectory : distributionDirectory, variant.target.name).version;
|
|
245
|
+
if (!satisfies(targetVersion, variant.target.range, { includePrerelease: true })) return [];
|
|
246
|
+
return [{
|
|
247
|
+
id,
|
|
248
|
+
patchPackage: {
|
|
249
|
+
name: variant.patchPackage.name,
|
|
250
|
+
version: variant.patchPackage.version
|
|
251
|
+
},
|
|
252
|
+
file: file.relativeFile,
|
|
253
|
+
absoluteFile: file.absoluteFile,
|
|
254
|
+
target: {
|
|
255
|
+
kind: "npm",
|
|
256
|
+
name: variant.target.name,
|
|
257
|
+
version: targetVersion
|
|
258
|
+
}
|
|
259
|
+
}];
|
|
260
|
+
});
|
|
261
|
+
const [candidate] = candidates;
|
|
262
|
+
if (candidate === void 0) throw new Error("no compatible patch variant for " + id + " on DSH " + dshVersion);
|
|
263
|
+
if (candidates.length > 1) throw new Error("multiple compatible patch variants for " + id + " on DSH " + dshVersion);
|
|
264
|
+
selected.push(candidate);
|
|
265
|
+
}
|
|
266
|
+
const lockPatches = selected.map((patch) => {
|
|
267
|
+
if ("alreadyApplied" in patch) {
|
|
268
|
+
const { absoluteFile: _absoluteFile, alreadyApplied: _alreadyApplied, ...locked } = patch;
|
|
269
|
+
return locked;
|
|
270
|
+
}
|
|
271
|
+
const { absoluteFile: _absoluteFile, ...locked } = patch;
|
|
272
|
+
return locked;
|
|
273
|
+
});
|
|
274
|
+
return {
|
|
275
|
+
patches: selected,
|
|
276
|
+
bundles,
|
|
277
|
+
runtimePackages,
|
|
278
|
+
profileDependencies,
|
|
279
|
+
allowBuilds,
|
|
280
|
+
lock: {
|
|
281
|
+
formatVersion: FORMAT_VERSION,
|
|
282
|
+
distribution: {
|
|
283
|
+
name: distributionName,
|
|
284
|
+
version: distributionVersion
|
|
285
|
+
},
|
|
286
|
+
dsh: {
|
|
287
|
+
version: dshVersion,
|
|
288
|
+
baseRevision,
|
|
289
|
+
checkoutRevision
|
|
290
|
+
},
|
|
291
|
+
runtimePackages,
|
|
292
|
+
patches: lockPatches
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
/** 保留每个独立payload,并为同一npm目标生成pnpm可消费的唯一组合patch。 */
|
|
297
|
+
function materializePatchFiles(profileDirectory, patches) {
|
|
298
|
+
const root = resolve(profileDirectory, ".dsh-plus", "patches");
|
|
299
|
+
rmSync(root, {
|
|
300
|
+
recursive: true,
|
|
301
|
+
force: true
|
|
302
|
+
});
|
|
303
|
+
const stablePatches = patches.map((patch) => {
|
|
304
|
+
const destination = resolve(root, patch.patchPackage.name, patch.patchPackage.version, patch.file);
|
|
305
|
+
mkdirSync(dirname(destination), { recursive: true });
|
|
306
|
+
copyFileSync(patch.absoluteFile, destination);
|
|
307
|
+
return {
|
|
308
|
+
...patch,
|
|
309
|
+
absoluteFile: destination
|
|
310
|
+
};
|
|
311
|
+
});
|
|
312
|
+
const npmGroups = /* @__PURE__ */ new Map();
|
|
313
|
+
for (const patch of stablePatches) {
|
|
314
|
+
if (!isSelectedNpmPatch(patch)) continue;
|
|
315
|
+
const key = patch.target.name + "@" + patch.target.version;
|
|
316
|
+
const group = npmGroups.get(key);
|
|
317
|
+
if (group === void 0) npmGroups.set(key, [patch]);
|
|
318
|
+
else group.push(patch);
|
|
319
|
+
}
|
|
320
|
+
const npmPatchFiles = /* @__PURE__ */ new Map();
|
|
321
|
+
for (const [key, group] of npmGroups) {
|
|
322
|
+
const first = group[0];
|
|
323
|
+
if (group.length === 1) {
|
|
324
|
+
npmPatchFiles.set(key, first.absoluteFile);
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
const members = group.map((patch) => encodeURIComponent(patch.id + "@" + patch.patchPackage.version)).join("+");
|
|
328
|
+
const destination = resolve(root, "combined", encodeURIComponent(key), members + ".patch");
|
|
329
|
+
mkdirSync(dirname(destination), { recursive: true });
|
|
330
|
+
writeFileSync(destination, group.map((patch) => {
|
|
331
|
+
const content = readFileSync(patch.absoluteFile, "utf8");
|
|
332
|
+
return content.endsWith("\n") ? content : content + "\n";
|
|
333
|
+
}).join(""));
|
|
334
|
+
npmPatchFiles.set(key, destination);
|
|
335
|
+
}
|
|
336
|
+
return {
|
|
337
|
+
patches: stablePatches,
|
|
338
|
+
npmPatchFiles
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
function profilePatchConfiguration(profileDirectory, npmPatchFiles) {
|
|
342
|
+
const document = parseDocument(readFileSync(resolve(profileDirectory, "pnpm-workspace.yaml"), "utf8"));
|
|
343
|
+
const [documentError] = document.errors;
|
|
344
|
+
if (documentError !== void 0) throw new Error("Plus profile workspace is not valid YAML", { cause: documentError });
|
|
345
|
+
let changed = false;
|
|
346
|
+
const desiredKeys = new Set(npmPatchFiles.keys());
|
|
347
|
+
const workspace = requireObject(document.toJS(), "Plus profile workspace");
|
|
348
|
+
const configured = workspace.patchedDependencies === void 0 ? {} : requireObject(workspace.patchedDependencies, "Plus profile patchedDependencies");
|
|
349
|
+
for (const [key, value] of Object.entries(configured)) if (typeof value === "string" && value.startsWith(".dsh-plus/patches/") && !desiredKeys.has(key)) {
|
|
350
|
+
document.deleteIn(["patchedDependencies", key]);
|
|
351
|
+
changed = true;
|
|
352
|
+
}
|
|
353
|
+
for (const [key, file] of npmPatchFiles) {
|
|
354
|
+
const value = relative(profileDirectory, file).replaceAll("\\", "/");
|
|
355
|
+
const current = document.getIn(["patchedDependencies", key]);
|
|
356
|
+
if (current !== void 0 && current !== value && (typeof current !== "string" || !current.startsWith(".dsh-plus/patches/"))) throw new Error("profile patchedDependencies." + key + " is already owned by another payload");
|
|
357
|
+
if (current === value) continue;
|
|
358
|
+
document.setIn(["patchedDependencies", key], value);
|
|
359
|
+
changed = true;
|
|
360
|
+
}
|
|
361
|
+
return {
|
|
362
|
+
text: String(document),
|
|
363
|
+
changed
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
function writeProfileRequirements(profileDirectory, dependencySpecs, allowBuilds) {
|
|
367
|
+
const manifestPath = resolve(profileDirectory, "package.json");
|
|
368
|
+
const manifest = readJsonObject(manifestPath, "DSH profile manifest");
|
|
369
|
+
const dependencies = requireObject(manifest.dependencies, "DSH profile dependencies");
|
|
370
|
+
let manifestChanged = false;
|
|
371
|
+
for (const [name, spec] of Object.entries(dependencySpecs)) {
|
|
372
|
+
const current = dependencies[name];
|
|
373
|
+
if (current !== void 0 && current !== spec) throw new Error("profile dependency " + name + " is configured as " + JSON.stringify(current) + ", expected " + spec);
|
|
374
|
+
if (current === void 0) {
|
|
375
|
+
dependencies[name] = spec;
|
|
376
|
+
manifestChanged = true;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
const workspacePath = resolve(profileDirectory, "pnpm-workspace.yaml");
|
|
380
|
+
const document = parseDocument(readFileSync(workspacePath, "utf8"));
|
|
381
|
+
const [documentError] = document.errors;
|
|
382
|
+
if (documentError !== void 0) throw new Error("Plus profile workspace is not valid YAML", { cause: documentError });
|
|
383
|
+
let workspaceChanged = false;
|
|
384
|
+
const autoInstallPeers = document.get("autoInstallPeers");
|
|
385
|
+
if (autoInstallPeers !== void 0 && autoInstallPeers !== false) throw new Error("profile autoInstallPeers must be false because official peers come from the selected DSH source");
|
|
386
|
+
if (autoInstallPeers === void 0) {
|
|
387
|
+
document.set("autoInstallPeers", false);
|
|
388
|
+
workspaceChanged = true;
|
|
389
|
+
}
|
|
390
|
+
const managedFileAsset = (value) => value.startsWith("file:") && (/[\\/]\.dsh[\\/]releases[\\/]plus[\\/].*[\\/]packages[\\/]/u.test(value) || /[\\/]\.dsh-plus[\\/]packages[\\/]/u.test(value));
|
|
391
|
+
const retiredOverrides = new Set([
|
|
392
|
+
"@huanlin/dsh-plugin-better-sidebar-plugin-office",
|
|
393
|
+
"@sparkelf/dsh-office-viewer-fonts",
|
|
394
|
+
"dsh-better-sidebar",
|
|
395
|
+
"dsh-video-preview"
|
|
396
|
+
]);
|
|
397
|
+
const workspace = requireObject(document.toJS(), "Plus profile workspace");
|
|
398
|
+
const configuredOverrides = workspace.overrides === void 0 ? {} : requireObject(workspace.overrides, "Plus profile overrides");
|
|
399
|
+
for (const [name, value] of Object.entries(configuredOverrides)) if (retiredOverrides.has(name) || typeof value === "string" && managedFileAsset(value)) {
|
|
400
|
+
document.deleteIn(["overrides", name]);
|
|
401
|
+
workspaceChanged = true;
|
|
402
|
+
}
|
|
403
|
+
for (const [name, allowed] of Object.entries(allowBuilds)) {
|
|
404
|
+
const current = document.getIn(["allowBuilds", name]);
|
|
405
|
+
if (current !== void 0 && current !== allowed) throw new Error("profile allowBuilds." + name + " is configured as " + JSON.stringify(current) + ", expected " + String(allowed));
|
|
406
|
+
if (current === void 0) {
|
|
407
|
+
document.setIn(["allowBuilds", name], allowed);
|
|
408
|
+
workspaceChanged = true;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
if (manifestChanged) {
|
|
412
|
+
manifest.dependencies = dependencies;
|
|
413
|
+
writeFileSync(manifestPath, JSON.stringify(manifest, void 0, 2) + "\n");
|
|
414
|
+
}
|
|
415
|
+
if (workspaceChanged) writeFileSync(workspacePath, String(document));
|
|
416
|
+
return manifestChanged || workspaceChanged;
|
|
417
|
+
}
|
|
418
|
+
function writeProfileComposition(profileDirectory, bundles, runtimePackages) {
|
|
419
|
+
const path = resolve(profileDirectory, "package.json");
|
|
420
|
+
const manifest = readJsonObject(path, "DSH profile manifest");
|
|
421
|
+
const dependencies = requireObject(manifest.dependencies, "DSH profile dependencies");
|
|
422
|
+
let changed = false;
|
|
423
|
+
for (const runtimePackage of runtimePackages) {
|
|
424
|
+
if (dependencies[runtimePackage.name] === void 0) {
|
|
425
|
+
dependencies[runtimePackage.name] = runtimePackage.version;
|
|
426
|
+
changed = true;
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
const installed = resolveInstalledPackage(profileDirectory, runtimePackage.name);
|
|
430
|
+
if (installed.version !== runtimePackage.version) throw new Error("profile dependency " + runtimePackage.name + " resolves to " + installed.version + ", but the Plus distribution requires " + runtimePackage.version);
|
|
431
|
+
}
|
|
432
|
+
const dsh = manifest.dsh === void 0 ? {} : requireObject(manifest.dsh, "DSH profile dsh");
|
|
433
|
+
const profile = dsh.profile === void 0 ? {} : requireObject(dsh.profile, "DSH profile dsh.profile");
|
|
434
|
+
if (JSON.stringify(profile.bundles) !== JSON.stringify(bundles)) changed = true;
|
|
435
|
+
manifest.dependencies = dependencies;
|
|
436
|
+
manifest.dsh = {
|
|
437
|
+
...dsh,
|
|
438
|
+
profile: {
|
|
439
|
+
...profile,
|
|
440
|
+
bundles: [...bundles]
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
if (changed) writeFileSync(path, JSON.stringify(manifest, void 0, 2) + "\n");
|
|
444
|
+
return changed;
|
|
445
|
+
}
|
|
446
|
+
function runPnpm(profileDirectory, args, label) {
|
|
447
|
+
const result = spawnSync("pnpm", args, {
|
|
448
|
+
cwd: profileDirectory,
|
|
449
|
+
stdio: "inherit",
|
|
450
|
+
shell: process.platform === "win32"
|
|
451
|
+
});
|
|
452
|
+
if (result.error !== void 0) throw result.error;
|
|
453
|
+
if (result.status !== 0) throw new Error(label + " failed with exit code " + String(result.status));
|
|
454
|
+
}
|
|
455
|
+
function pendingSourcePatchFiles(patches) {
|
|
456
|
+
return patches.flatMap((patch) => "alreadyApplied" in patch && !patch.alreadyApplied ? [patch.absoluteFile] : []);
|
|
457
|
+
}
|
|
458
|
+
function applySelectedPatches(profileDirectory, dshRoot, patches, profileChanged) {
|
|
459
|
+
const materialized = materializePatchFiles(profileDirectory, patches);
|
|
460
|
+
const workspacePath = resolve(profileDirectory, "pnpm-workspace.yaml");
|
|
461
|
+
const workspace = profilePatchConfiguration(profileDirectory, materialized.npmPatchFiles);
|
|
462
|
+
if (workspace.changed) writeFileSync(workspacePath, workspace.text);
|
|
463
|
+
if (profileChanged || workspace.changed) runPnpm(profileDirectory, ["install", "--no-frozen-lockfile"], "pnpm install in Plus profile");
|
|
464
|
+
const sourceFiles = pendingSourcePatchFiles(materialized.patches);
|
|
465
|
+
if (sourceFiles.length > 0) git(dshRoot, ["update-index", "--refresh"], true);
|
|
466
|
+
for (const file of sourceFiles) git(dshRoot, [
|
|
467
|
+
"apply",
|
|
468
|
+
"--3way",
|
|
469
|
+
file
|
|
470
|
+
]);
|
|
471
|
+
if (materialized.patches.some((patch) => patch.target.kind === "dsh-source")) {
|
|
472
|
+
if (sourceFiles.length > 0) runPnpm(dshRoot, ["install", "--no-frozen-lockfile"], "pnpm install after source patches");
|
|
473
|
+
runPnpm(dshRoot, ["run", "build:official"], "official DSH build after source patches");
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
function officialPackageRoot(dshRoot) {
|
|
477
|
+
return resolve(dshRoot, "apps", "cli");
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Parse pnpm's workspace roster into exact official package directories.
|
|
481
|
+
* @param dshRoot - exact official DSH checkout root.
|
|
482
|
+
* @param stdout - JSON emitted by `pnpm -r list --json --depth -1`.
|
|
483
|
+
* @returns sorted official package names and directories inside the checkout.
|
|
484
|
+
*/
|
|
485
|
+
function parseOfficialWorkspacePackages(dshRoot, stdout) {
|
|
486
|
+
const parsed = JSON.parse(stdout);
|
|
487
|
+
if (!Array.isArray(parsed)) throw new Error("pnpm workspace package list must be an array");
|
|
488
|
+
const packages = /* @__PURE__ */ new Map();
|
|
489
|
+
for (const [index, value] of parsed.entries()) {
|
|
490
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error("pnpm workspace package list[" + String(index) + "] must be an object");
|
|
491
|
+
const entry = value;
|
|
492
|
+
if (typeof entry.name !== "string" || !entry.name.startsWith("@deepseek-ai/")) continue;
|
|
493
|
+
const localName = entry.name.slice(13);
|
|
494
|
+
if (localName === "" || localName.includes("/")) throw new Error("unsupported official workspace package name: " + entry.name);
|
|
495
|
+
if (typeof entry.path !== "string" || entry.path === "") throw new Error("pnpm workspace package " + entry.name + " has no path");
|
|
496
|
+
const directory = resolve(dshRoot, entry.path);
|
|
497
|
+
const local = relative(dshRoot, directory);
|
|
498
|
+
if (isAbsolute(local) || local === ".." || local.startsWith("../")) throw new Error("official workspace package escapes DSH root: " + entry.name);
|
|
499
|
+
const previous = packages.get(entry.name);
|
|
500
|
+
if (previous !== void 0 && previous !== directory) throw new Error("duplicate official workspace package: " + entry.name);
|
|
501
|
+
packages.set(entry.name, directory);
|
|
502
|
+
}
|
|
503
|
+
return [...packages].sort(([left], [right]) => left.localeCompare(right)).map(([name, directory]) => ({
|
|
504
|
+
name,
|
|
505
|
+
directory
|
|
506
|
+
}));
|
|
507
|
+
}
|
|
508
|
+
function listOfficialWorkspacePackages(dshRoot) {
|
|
509
|
+
const result = spawnSync("pnpm", [
|
|
510
|
+
"-r",
|
|
511
|
+
"list",
|
|
512
|
+
"--json",
|
|
513
|
+
"--depth",
|
|
514
|
+
"-1"
|
|
515
|
+
], {
|
|
516
|
+
cwd: dshRoot,
|
|
517
|
+
encoding: "utf8",
|
|
518
|
+
shell: process.platform === "win32"
|
|
519
|
+
});
|
|
520
|
+
if (result.error !== void 0) throw result.error;
|
|
521
|
+
if (result.status !== 0) throw new Error("pnpm list official DSH workspace packages failed with exit code " + String(result.status));
|
|
522
|
+
return parseOfficialWorkspacePackages(dshRoot, result.stdout);
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Build the profile's official scope from the CLI closure plus source workspaces.
|
|
526
|
+
* @param profileDirectory - materialized Plus profile root.
|
|
527
|
+
* @param cliScope - official packages already installed under the DSH CLI.
|
|
528
|
+
* @param workspacePackages - additional official source workspaces required by external peers.
|
|
529
|
+
*/
|
|
530
|
+
function materializeOfficialPackageScope(profileDirectory, cliScope, workspacePackages) {
|
|
531
|
+
if (!existsSync(cliScope)) throw new Error("official DSH dependencies are missing under " + cliScope);
|
|
532
|
+
const destination = resolve(profileDirectory, "node_modules", "@deepseek-ai");
|
|
533
|
+
rmSync(destination, {
|
|
534
|
+
recursive: true,
|
|
535
|
+
force: true
|
|
536
|
+
});
|
|
537
|
+
mkdirSync(destination, { recursive: true });
|
|
538
|
+
const linkType = process.platform === "win32" ? "junction" : "dir";
|
|
539
|
+
for (const name of readdirSync(cliScope).sort()) symlinkSync(resolve(cliScope, name), resolve(destination, name), linkType);
|
|
540
|
+
for (const entry of workspacePackages) {
|
|
541
|
+
const target = resolve(destination, entry.name.slice(13));
|
|
542
|
+
rmSync(target, {
|
|
543
|
+
recursive: true,
|
|
544
|
+
force: true
|
|
545
|
+
});
|
|
546
|
+
symlinkSync(entry.directory, target, linkType);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
function linkOfficialPackages(profileDirectory, dshRoot) {
|
|
550
|
+
materializeOfficialPackageScope(profileDirectory, resolve(officialPackageRoot(dshRoot), "node_modules", "@deepseek-ai"), listOfficialWorkspacePackages(dshRoot));
|
|
551
|
+
}
|
|
552
|
+
function writeLock(profileDirectory, lock) {
|
|
553
|
+
const directory = resolve(profileDirectory, ".dsh-plus");
|
|
554
|
+
mkdirSync(directory, { recursive: true });
|
|
555
|
+
const path = resolve(directory, "patchset.lock.json");
|
|
556
|
+
writeFileSync(path, JSON.stringify(lock, void 0, 2) + "\n", { mode: 384 });
|
|
557
|
+
chmodSync(path, 384);
|
|
558
|
+
return path;
|
|
559
|
+
}
|
|
560
|
+
function parseApplyArguments(argv) {
|
|
561
|
+
if (argv[0] !== "apply") throw new Error("usage: dsh-plus apply --dsh-root <official-dsh-root>");
|
|
562
|
+
let dshRoot;
|
|
563
|
+
for (let index = 1; index < argv.length; index += 2) {
|
|
564
|
+
const key = argv[index];
|
|
565
|
+
const value = argv[index + 1];
|
|
566
|
+
if (key !== "--dsh-root" || value === void 0) throw new Error("usage: dsh-plus apply --dsh-root <official-dsh-root>");
|
|
567
|
+
dshRoot = resolve(value);
|
|
568
|
+
}
|
|
569
|
+
if (dshRoot === void 0) throw new Error("usage: dsh-plus apply --dsh-root <official-dsh-root>");
|
|
570
|
+
return {
|
|
571
|
+
dshRoot,
|
|
572
|
+
profileDirectory: process.cwd()
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Apply the installed Plus distribution metadata from a DSH profile directory.
|
|
577
|
+
* @param argv - Explicit `apply --dsh-root <path>` command arguments.
|
|
578
|
+
*/
|
|
579
|
+
function runApply(argv) {
|
|
580
|
+
const { dshRoot, profileDirectory } = parseApplyArguments(argv);
|
|
581
|
+
if (requireObject(readJsonObject(resolve(profileDirectory, "package.json"), "DSH profile manifest").dependencies, "DSH profile dependencies")["@sparkelf/dsh-plus"] === void 0) throw new Error("current directory is not a DSH profile with @sparkelf/dsh-plus installed");
|
|
582
|
+
const selection = prepareSelection(dirname(createRequire(resolve(profileDirectory, "package.json")).resolve("@sparkelf/dsh-plus/package.json")), profileDirectory, dshRoot);
|
|
583
|
+
const profileChanged = writeProfileComposition(profileDirectory, selection.bundles, selection.runtimePackages);
|
|
584
|
+
applySelectedPatches(profileDirectory, dshRoot, selection.patches, profileChanged);
|
|
585
|
+
linkOfficialPackages(profileDirectory, dshRoot);
|
|
586
|
+
const installedProfileDependencies = requireObject(readJsonObject(resolve(profileDirectory, "package.json"), "DSH profile manifest").dependencies, "DSH profile dependencies");
|
|
587
|
+
selection.lock.profile = {
|
|
588
|
+
bundles: selection.bundles.map((name) => {
|
|
589
|
+
return {
|
|
590
|
+
name,
|
|
591
|
+
version: (installedProfileDependencies[name] === void 0 ? resolveInstalledPackage(officialPackageRoot(dshRoot), name) : resolveInstalledPackage(profileDirectory, name)).version
|
|
592
|
+
};
|
|
593
|
+
}),
|
|
594
|
+
dependencies: selection.profileDependencies,
|
|
595
|
+
allowBuilds: selection.allowBuilds
|
|
596
|
+
};
|
|
597
|
+
const lock = writeLock(profileDirectory, selection.lock);
|
|
598
|
+
process.stdout.write("[dsh-plus] applied " + String(selection.patches.length) + " patches; lock " + lock + "\n");
|
|
599
|
+
}
|
|
600
|
+
//#endregion
|
|
601
|
+
export { parseOfficialWorkspacePackages as n, runApply as r, materializeOfficialPackageScope as t };
|