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