@sparkelf/dsh-plus 0.2.1-rc.2.4 → 0.2.2-alpha.2.1

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