@sparkelf/dsh-plus 0.2.4-alpha.48 → 0.2.6-alpha.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,12 +9,12 @@
9
9
  * bundle's own list.
10
10
  */
11
11
  import { spawnSync } from 'node:child_process';
12
- import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
12
+ import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
13
13
  import { createRequire } from 'node:module';
14
- import { fileURLToPath } from 'node:url';
15
14
  import { homedir } from 'node:os';
16
15
  import { dirname, join, posix, resolve, win32 } from 'node:path';
17
16
  import { parseDocument } from 'yaml';
17
+ import { projectEdition, readDistributionFacts } from "./manifest.js";
18
18
  import { readPatchSet } from "./patch-set.js";
19
19
  /** Profile name a standalone installation owns when its package declares none. */
20
20
  export const STANDALONE_PROFILE = 'plus';
@@ -94,16 +94,29 @@ function requireRecord(value, label) {
94
94
  throw new Error(label + ' must be an object');
95
95
  return value;
96
96
  }
97
- function requireStringArray(value, label) {
98
- if (!Array.isArray(value) || value.some(entry => typeof entry !== 'string'))
99
- throw new Error(label + ' must be a string array');
100
- return value;
101
- }
102
- /** Read one non-empty string field. */
103
- function requireString(value, label) {
104
- if (typeof value !== 'string' || value.trim() === '')
105
- throw new Error(label + ' must be a non-empty string');
106
- return value;
97
+ /**
98
+ * Resolve the file that was actually invoked from the path the shell was handed.
99
+ *
100
+ * A global npm install links `<prefix>/bin/dsh-plus` to the package entry. Resolving
101
+ * `@sparkelf/dsh-plus` from that link starts at the bin directory, which has no
102
+ * `node_modules`, so every globally installed command reported the distribution as
103
+ * missing. The real path is the package file, and the package resolves its own name.
104
+ *
105
+ * @param invoked - `process.argv[1]`, or undefined when node ran without a script.
106
+ * @param fallback - this module's own file, used when there is no script path.
107
+ * @returns an absolute path that exists, or the input when it cannot be resolved.
108
+ */
109
+ export function resolveInvocationAnchor(invoked, fallback) {
110
+ if (invoked === undefined || invoked === '')
111
+ return realpathSync(fallback);
112
+ try {
113
+ return realpathSync(invoked);
114
+ }
115
+ catch {
116
+ // A path the filesystem no longer knows cannot be resolved; the caller's error message is
117
+ // more useful with what it was given than with a second failure here.
118
+ return resolve(invoked);
119
+ }
107
120
  }
108
121
  /** Resolve the DSH home, honouring the environment override the launcher itself reads. */
109
122
  export function resolveHome(env = process.env) {
@@ -143,15 +156,12 @@ export function isWithin(candidate, parent, platform = process.platform) {
143
156
  * @returns absolute path to the installed distribution.
144
157
  */
145
158
  export function resolveDistributionDirectory(anchor) {
146
- const ownDirectory = dirname(dirname(fileURLToPath(import.meta.url)));
147
159
  let resolved;
148
160
  try {
149
161
  resolved = dirname(createRequire(anchor).resolve('@sparkelf/dsh-plus/package.json'));
150
162
  }
151
163
  catch {
152
- // `npx @sparkelf/dsh-plus ...` runs the CLI from a temporary install, so the consumer
153
- // tree has no copy to resolve. The running module's own package is the distribution.
154
- return ownDirectory;
164
+ throw new Error('@sparkelf/dsh-plus is not installed in ' + dirname(anchor) + '; run this command from the directory that installed it');
155
165
  }
156
166
  // A resolution that leaves the consumer tree means the module answered from its own
157
167
  // installation, which reports health for a distribution the consumer never installed.
@@ -167,64 +177,25 @@ export function resolveDistributionDirectory(anchor) {
167
177
  break;
168
178
  current = parent;
169
179
  }
170
- return ownDirectory;
180
+ throw new Error('@sparkelf/dsh-plus resolved outside ' + resolve(dirname(anchor)) + ' (found ' + resolved + '); run this command from the directory that installed it');
171
181
  }
172
- export function readDistributionProfile(distributionDirectory) {
173
- const manifest = requireRecord(JSON.parse(readFileSync(join(distributionDirectory, 'package.json'), 'utf8')), 'Plus distribution manifest');
174
- const plus = requireRecord(manifest.dshPlus, 'dshPlus');
175
- const profile = requireRecord(plus.profile, 'dshPlus.profile');
176
- const dependencies = requireDependencyMap(requireRecord(profile.dependencies, 'dshPlus.profile.dependencies'), 'dshPlus.profile.dependencies');
177
- const rawAllowBuilds = requireRecord(profile.allowBuilds, 'dshPlus.profile.allowBuilds');
178
- const allowBuilds = {};
179
- for (const [name, allowed] of Object.entries(rawAllowBuilds)) {
180
- if (typeof allowed !== 'boolean')
181
- throw new Error('dshPlus.profile.allowBuilds.' + name + ' must be a boolean');
182
- allowBuilds[name] = allowed;
183
- }
184
- // An override substitutes our republished build for an official package by name: the
185
- // built code imports the official specifier, so the installed location has to keep
186
- // that name while its contents come from ours.
187
- const overrides = {};
188
- const rawOverrides = profile.overrides === undefined
189
- ? {}
190
- : requireRecord(profile.overrides, 'dshPlus.profile.overrides');
191
- for (const [name, spec] of Object.entries(rawOverrides)) {
192
- if (typeof spec !== 'string' || spec === '')
193
- throw new Error('dshPlus.profile.overrides.' + name + ' must be a non-empty string');
194
- overrides[name] = spec;
195
- }
196
- const editions = {};
197
- const rawEditions = plus.editions === undefined ? {} : requireRecord(plus.editions, 'dshPlus.editions');
198
- for (const [id, value] of Object.entries(rawEditions)) {
199
- const edition = requireRecord(value, 'dshPlus.editions.' + id);
200
- editions[id] = {
201
- label: requireString(edition.label, 'dshPlus.editions.' + id + '.label'),
202
- bundles: requireStringArray(edition.bundles, 'dshPlus.editions.' + id + '.bundles'),
203
- packages: requireDependencyMap(requireRecord(edition.packages, 'dshPlus.editions.' + id + '.packages'), 'dshPlus.editions.' + id + '.packages'),
204
- };
205
- }
206
- const compatibility = requireRecord(plus.compatibility, 'dshPlus.compatibility');
182
+ /** The reviewed bundle order and pins the installed distribution declares, for one variant. */
183
+ export function readDistributionProfile(distributionDirectory, editionId, extraExcludes = []) {
184
+ const facts = readDistributionFacts(distributionDirectory);
185
+ const projection = projectEdition(facts, editionId, extraExcludes);
207
186
  return {
208
- name: String(manifest.name),
209
- bundles: requireStringArray(profile.bundles, 'dshPlus.profile.bundles'),
210
- dependencies,
211
- allowBuilds,
212
- overrides,
213
- editions,
214
- dshRange: String(compatibility.dsh),
215
- version: String(manifest.version),
187
+ name: facts.name,
188
+ version: facts.version,
189
+ dshRange: '>=' + facts.runtimeVersion,
190
+ runtimeVersion: facts.runtimeVersion,
191
+ bundles: projection.bundles,
192
+ dependencies: projection.dependencies,
193
+ allowBuilds: projection.allowBuilds,
194
+ overrides: projection.overrides,
195
+ omittedPackages: projection.omittedPackages,
196
+ editions: facts.editions,
216
197
  };
217
198
  }
218
- /** Read one package-name-to-spec map, rejecting empty entries. */
219
- function requireDependencyMap(value, label) {
220
- const dependencies = {};
221
- for (const [name, spec] of Object.entries(value)) {
222
- if (typeof spec !== 'string' || spec === '')
223
- throw new Error(label + '.' + name + ' must be a non-empty string');
224
- dependencies[name] = spec;
225
- }
226
- return dependencies;
227
- }
228
199
  /**
229
200
  * Give the profile its own installed tree, built from the distribution's declarations.
230
201
  *
@@ -241,18 +212,11 @@ function requireDependencyMap(value, label) {
241
212
  * twice; `dsh-plus apply` remains the command that reinstalls after a change.
242
213
  *
243
214
  * @param paths - resolved standalone paths.
244
- * @param consumerDirectory - directory whose `node_modules` holds the installation.
245
215
  */
246
- export function installProfilePackages(paths, consumerDirectory) {
247
- const consumerModules = join(consumerDirectory, 'node_modules');
248
- if (!existsSync(consumerModules)) {
249
- // `npx @sparkelf/dsh-plus` runs from a temporary package tree that has no launcher
250
- // beside it. The profile installs its own tree, so there is nothing to link from
251
- // the caller; only the profile's packages matter.
252
- installWithRegistryFallback(paths);
253
- alignReplacedPackageNames(join(paths.profileDirectory, 'node_modules'));
254
- return;
255
- }
216
+ export function installProfilePackages(paths, omitted = new Set(), runtimeVersion, registry) {
217
+ // The profile owns its dependency tree: it declares the launcher and every plugin, so it can
218
+ // install without a consumer tree beside it. A globally installed distribution has none --
219
+ // its only node_modules is the one npm laid out around the command itself.
256
220
  const profileModules = join(paths.profileDirectory, 'node_modules');
257
221
  if (existsSync(profileModules)) {
258
222
  // A profile installed before the distribution declared overrides still needs the
@@ -260,9 +224,142 @@ export function installProfilePackages(paths, consumerDirectory) {
260
224
  linkNestedBundles(paths, profileModules);
261
225
  return;
262
226
  }
263
- installWithRegistryFallback(paths);
264
- alignReplacedPackageNames(profileModules);
227
+ installWithRegistryFallback(paths.profileDirectory);
228
+ // A range admits the next prerelease, so the official family lands one generation ahead of
229
+ // the runtime the republished builds were cut against. Realign, then finish the tree.
230
+ if (runtimeVersion !== undefined && registry !== undefined) {
231
+ alignOfficialVersions(paths.profileDirectory, runtimeVersion, registry);
232
+ }
233
+ alignReplacedPackageNames(profileModules, omitted);
265
234
  linkNestedBundles(paths, profileModules);
235
+ assertProfileBundles(paths.profileDirectory);
236
+ }
237
+ /** Package names a package declares as peers on the official runtime family. */
238
+ function officialPeers(profileDirectory) {
239
+ const peers = new Set();
240
+ const manifests = [];
241
+ for (const scope of [join(profileDirectory, 'node_modules'), join(profileDirectory, 'node_modules', '@deepseek-ai')]) {
242
+ if (!existsSync(scope))
243
+ continue;
244
+ for (const entry of readdirSync(scope)) {
245
+ if (entry.startsWith('.'))
246
+ continue;
247
+ if (entry.startsWith('@') && entry !== '@deepseek-ai')
248
+ continue;
249
+ if (entry === '@deepseek-ai') {
250
+ for (const scoped of readdirSync(scope))
251
+ manifests.push(join(scope, scoped, 'package.json'));
252
+ continue;
253
+ }
254
+ manifests.push(join(scope, entry, 'package.json'));
255
+ }
256
+ }
257
+ for (const manifestPath of manifests) {
258
+ if (!existsSync(manifestPath))
259
+ continue;
260
+ try {
261
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
262
+ for (const peer of Object.keys(manifest.peerDependencies ?? {})) {
263
+ if (peer.startsWith('@deepseek-ai/dsh') && peer !== '@deepseek-ai/dsh')
264
+ peers.add(peer);
265
+ }
266
+ }
267
+ catch {
268
+ // A package whose manifest cannot be read declares no peer this step can act on.
269
+ }
270
+ }
271
+ return peers;
272
+ }
273
+ /**
274
+ * Which of the given package versions a registry serves.
275
+ *
276
+ * The probe runs in one Node child: one process per name is slow, and the caller needs the
277
+ * answer before the second install.
278
+ *
279
+ * @param names - package names to check.
280
+ * @param version - exact version each must serve.
281
+ * @param registry - registry base URL.
282
+ * @returns the names the registry serves at that version.
283
+ */
284
+ function servedVersions(names, version, registry) {
285
+ if (names.length === 0)
286
+ return new Set();
287
+ const script = `
288
+ const [names, version, registry] = JSON.parse(process.argv[1])
289
+ const base = registry.replace(/\\/$/, '')
290
+ const queue = [...names]
291
+ const served = []
292
+ await Promise.all(Array.from({ length: 10 }, async () => {
293
+ while (queue.length > 0) {
294
+ const name = queue.shift()
295
+ try {
296
+ const response = await fetch(base + '/' + name.replace('/', '%2f') + '/' + version)
297
+ if (response.ok) served.push(name)
298
+ } catch {}
299
+ }
300
+ }))
301
+ process.stdout.write(served.join('\\n'))
302
+ `;
303
+ const result = spawnSync(process.execPath, ['--input-type=module', '-e', script, JSON.stringify([names, version, registry])], { encoding: 'utf8' });
304
+ if (result.status !== 0)
305
+ return new Set();
306
+ return new Set(result.stdout.split('\n').map(entry => entry.trim()).filter(entry => entry !== ''));
307
+ }
308
+ /**
309
+ * Move the shared official services onto the runtime's exact version.
310
+ *
311
+ * The republished builds were cut against one runtime, and DSH disables a row when the exact
312
+ * peers a plugin declares disagree with the process it shares. Plugins declare those services as
313
+ * peers, so the names worth pinning are the peers actually installed -- read from the tree and
314
+ * checked against the registry. The rest of the official family keeps its resolved versions:
315
+ * pinning everything traded one mismatch for a tree with holes.
316
+ *
317
+ * @param profileDirectory - profile whose tree was just installed.
318
+ * @param runtimeVersion - exact runtime the profile boots.
319
+ * @param registry - registry the profile installs from.
320
+ * @returns how many services were realigned.
321
+ */
322
+ export function alignOfficialVersions(profileDirectory, runtimeVersion, registry) {
323
+ const wanted = [...officialPeers(profileDirectory)];
324
+ const served = servedVersions(wanted, runtimeVersion, registry);
325
+ if (served.size === 0)
326
+ return 0;
327
+ const workspacePath = join(profileDirectory, 'pnpm-workspace.yaml');
328
+ const document = parseDocument(readFileSync(workspacePath, 'utf8'));
329
+ let changed = 0;
330
+ for (const name of served) {
331
+ if (document.getIn(['overrides', name]) === runtimeVersion)
332
+ continue;
333
+ document.setIn(['overrides', name], runtimeVersion);
334
+ changed += 1;
335
+ }
336
+ if (changed === 0)
337
+ return 0;
338
+ writeFileSync(workspacePath, String(document));
339
+ rmSync(join(profileDirectory, 'pnpm-lock.yaml'), { force: true });
340
+ installWithRegistryFallback(profileDirectory);
341
+ return changed;
342
+ }
343
+ /**
344
+ * Assert every bundle row the profile declares resolves in its installed tree.
345
+ *
346
+ * A row without an installed package mounts nothing and reports nothing: the client module
347
+ * system simply never registers it, and every earlier step passes. Reading the rows back from
348
+ * the manifest on disk rather than from the projection is what makes this cover the tree the
349
+ * launcher actually boots.
350
+ *
351
+ * @param profileDirectory - profile whose node_modules was just installed.
352
+ */
353
+ export function assertProfileBundles(profileDirectory) {
354
+ const manifest = JSON.parse(readFileSync(join(profileDirectory, 'package.json'), 'utf8'));
355
+ const missing = [];
356
+ for (const name of manifest.dsh?.profile?.bundles ?? []) {
357
+ if (!existsSync(join(profileDirectory, 'node_modules', name, 'package.json')))
358
+ missing.push(name);
359
+ }
360
+ if (missing.length > 0) {
361
+ throw new Error('the profile mounts bundles it did not install: ' + missing.join(', '));
362
+ }
266
363
  }
267
364
  /**
268
365
  * Install the profile from the first registry that answers.
@@ -273,21 +370,13 @@ export function installProfilePackages(paths, consumerDirectory) {
273
370
  *
274
371
  * @param paths - resolved standalone paths.
275
372
  */
276
- function installWithRegistryFallback(paths) {
373
+ function installWithRegistryFallback(profileDirectory) {
277
374
  const registries = registryOrder();
278
375
  for (const [index, registry] of registries.entries()) {
279
- // A lockfile written against the previous registry keeps its resolved URLs, so the
280
- // second attempt would fetch from the registry that just failed. Drop it and resolve
281
- // against the registry being tried.
282
- if (index > 0)
283
- rmSync(join(paths.profileDirectory, 'pnpm-lock.yaml'), { force: true });
284
376
  const result = spawnSync('pnpm', ['install', '--no-frozen-lockfile', '--registry', registry], {
285
- cwd: paths.profileDirectory,
377
+ cwd: profileDirectory,
286
378
  stdio: 'inherit',
287
379
  shell: process.platform === 'win32',
288
- // A registry that accepts the connection but never answers would otherwise hold the
289
- // first start for pnpm's full retry budget before the fallback got a chance.
290
- timeout: 5 * 60_000,
291
380
  });
292
381
  if (result.error !== undefined)
293
382
  throw result.error;
@@ -316,22 +405,54 @@ function installWithRegistryFallback(paths) {
316
405
  *
317
406
  * @param profileModules - the profile's \`node_modules\` directory.
318
407
  */
319
- export function alignReplacedPackageNames(profileModules) {
320
- const scoped = join(profileModules, '@deepseek-ai');
321
- if (!existsSync(scoped))
408
+ export function alignReplacedPackageNames(profileModules, omitted = new Set()) {
409
+ if (!existsSync(profileModules))
322
410
  return;
323
- for (const entry of readdirSync(scoped)) {
324
- const manifestPath = join(scoped, entry, 'package.json');
411
+ // A replaced package is installed at the name its loader row declares, but the manifest inside
412
+ // still names our scope: the client module system requires the directory's package to declare
413
+ // the directory name, so the panels it renders would never register. Both scopes are walked
414
+ // because a row can be an unscoped alias (`dsh-better-sidebar`) onto a scoped build.
415
+ const align = (expected, manifestPath) => {
325
416
  if (!existsSync(manifestPath))
326
- continue;
417
+ return;
327
418
  const declared = JSON.parse(readFileSync(manifestPath, 'utf8'));
328
- const expected = '@deepseek-ai/' + entry;
329
- if (declared.name === expected)
330
- continue;
331
- const replaced = { ...declared, name: expected };
419
+ if (declared.name === expected || omitted.has(expected))
420
+ return;
332
421
  const temporary = manifestPath + '.dsh-name';
333
- writeFileSync(temporary, JSON.stringify(replaced, null, 2) + '\n');
422
+ writeFileSync(temporary, JSON.stringify({ ...declared, name: expected }, null, 2) + '\n');
334
423
  renameSync(temporary, manifestPath);
424
+ };
425
+ for (const entry of readdirSync(profileModules)) {
426
+ if (entry.startsWith('.'))
427
+ continue;
428
+ if (!entry.startsWith('@')) {
429
+ // An unscoped row is an npm alias onto a scoped build. The package imports its real
430
+ // scoped name, so renaming it would break its own imports; exposing the scoped name as
431
+ // a link is what makes both the row and the package resolve.
432
+ const manifestPath = join(profileModules, entry, 'package.json');
433
+ if (!existsSync(manifestPath))
434
+ continue;
435
+ const declared = JSON.parse(readFileSync(manifestPath, 'utf8'));
436
+ if (declared.name === undefined || declared.name === entry)
437
+ continue;
438
+ const [scope, name] = declared.name.split('/');
439
+ if (scope === undefined || name === undefined)
440
+ continue;
441
+ mkdirSync(join(profileModules, scope), { recursive: true });
442
+ const link = join(profileModules, scope, name);
443
+ if (existsSync(link))
444
+ continue;
445
+ try {
446
+ symlinkSync(join('..', entry), link, 'junction');
447
+ }
448
+ catch {
449
+ // A concurrent install may have created the same link; the existing one is equivalent.
450
+ }
451
+ continue;
452
+ }
453
+ for (const scoped of readdirSync(join(profileModules, entry))) {
454
+ align(entry + '/' + scoped, join(profileModules, entry, scoped, 'package.json'));
455
+ }
335
456
  }
336
457
  }
337
458
  /** The npm registry the distribution installs from by default. */
@@ -350,12 +471,8 @@ const MAINLAND_REGISTRY = 'https://registry.npmmirror.com';
350
471
  */
351
472
  export function registryOrder() {
352
473
  const configured = process.env.DSH_PLUS_INSTALL_REGISTRY;
353
- if (configured !== undefined && configured !== '') {
354
- // The fallback must be the other registry, not the configured one again: an override
355
- // set to the mirror used to fall back to the same mirror and fail twice.
356
- const fallback = configured === MAINLAND_REGISTRY ? OFFICIAL_REGISTRY : MAINLAND_REGISTRY;
357
- return [configured, fallback];
358
- }
474
+ if (configured !== undefined && configured !== '')
475
+ return [configured, MAINLAND_REGISTRY];
359
476
  // Intl reports the system locale on every platform. The POSIX variables are empty on
360
477
  // Windows, so reading them alone classified every Windows console as non-mainland and
361
478
  // reached the origin first — measured in a consumer's log, which showed npmjs failing
@@ -447,15 +564,30 @@ function linkBundle(consumerModules, name, source) {
447
564
  }
448
565
  }
449
566
  /** Resolve every path a command needs, without creating anything. */
450
- export function resolvePaths(anchor, env = process.env) {
567
+ export function resolvePaths(anchor, env = process.env, editionId) {
451
568
  const home = resolveHome(env);
452
- const declaration = readStandaloneDeclaration(anchor);
569
+ const distributionDirectory = resolveDistributionDirectory(anchor);
570
+ // An edition is a fact of the distribution now, so the profile name and the omission set come
571
+ // from it. Without one, a legacy forwarder package may still declare them; the distribution's
572
+ // own default profile is the fallback.
573
+ let profileName = STANDALONE_PROFILE;
574
+ let omittedPackages = {};
575
+ if (editionId !== undefined) {
576
+ const projection = projectEdition(readDistributionFacts(distributionDirectory), editionId);
577
+ profileName = projection.profileName;
578
+ omittedPackages = projection.omittedPackages;
579
+ }
580
+ else {
581
+ const declaration = readStandaloneDeclaration(anchor);
582
+ profileName = declaration.profileName;
583
+ omittedPackages = declaration.omittedPackages;
584
+ }
453
585
  return {
454
586
  home,
455
- profileName: declaration.profileName,
456
- omittedPackages: declaration.omittedPackages,
457
- profileDirectory: join(home, 'profiles', declaration.profileName),
458
- distributionDirectory: resolveDistributionDirectory(anchor),
587
+ profileName,
588
+ omittedPackages,
589
+ profileDirectory: join(home, 'profiles', profileName),
590
+ distributionDirectory,
459
591
  };
460
592
  }
461
593
  /**
@@ -471,140 +603,51 @@ export function resolvePaths(anchor, env = process.env) {
471
603
  * that installed them.
472
604
  *
473
605
  * @param paths - resolved standalone paths.
474
- * @param consumerDirectory - directory whose `node_modules` holds the installed packages.
475
- * @param edition - edition id whose package list to install, or undefined for the default.
606
+ * @param editionId - product edition to materialize.
607
+ * @param extraExcludes - packages the caller drops on top of the edition's own exclusions.
608
+ * @param dryRun - write the profile files without installing their dependency tree.
476
609
  * @returns whether this call created the manifest.
477
610
  */
478
- export function ensureProfile(paths, consumerDirectory, edition) {
611
+ export function ensureProfile(paths, editionId, extraExcludes = [], dryRun = false) {
479
612
  const manifestPath = join(paths.profileDirectory, 'package.json');
480
- const distribution = readDistributionProfile(paths.distributionDirectory);
481
- const composition = edition === undefined ? undefined : distribution.editions[edition];
482
- if (edition !== undefined && composition === undefined) {
483
- const offered = Object.keys(distribution.editions);
484
- throw new Error('unknown edition "' + edition + '"; this release offers '
485
- + (offered.length === 0 ? '(none)' : offered.join(', ')));
486
- }
487
- // An edition lists only third-party, fork, and self-developed plugins. The official
488
- // runtime is the CLI's responsibility and must not be duplicated in that list.
489
- for (const name of Object.keys(composition?.packages ?? {})) {
490
- if (name.startsWith('@deepseek-ai/')) {
491
- throw new Error('edition "' + String(edition) + '" lists the official package ' + name
492
- + '; editions contain only third-party or self-developed plugins');
493
- }
494
- }
495
- const runtime = distribution.dshRange.replace(/^>=?/u, '');
496
- const baseBundles = composition === undefined
497
- ? distribution.bundles
498
- : distribution.bundles.filter(bundle => bundle.startsWith('@deepseek-ai/') || bundle === '@sparkelf/dsh-plus');
499
- const basePackages = composition === undefined
500
- ? distribution.dependencies
501
- : {
502
- '@deepseek-ai/dsh': distribution.dshRange,
503
- '@sparkelf/dsh-plus': distribution.version,
504
- // `dsh-app-boot` declares its launcher as peer dependencies, and pnpm does not
505
- // install a profile's transitive peers. Without these the boot loader resolves
506
- // `@deepseek-ai/cordis-plugin-group` and fails before any plugin mounts.
507
- '@deepseek-ai/cordis': '^4.0.6-alpha.1',
508
- '@deepseek-ai/cordis-plugin-group': '^1.0.6-alpha.1',
509
- '@deepseek-ai/cordis-plugin-include': '^1.0.11-alpha.1',
510
- '@deepseek-ai/cordis-plugin-loader': '^1.0.7-alpha.1',
511
- '@deepseek-ai/dsh-home-paths': runtime,
512
- '@deepseek-ai/dsh-launch-environment': runtime,
513
- // `dsh-session` / `dsh-tools` declare the rest of the runtime as peers; pnpm leaves
514
- // them out of a profile install for the same reason as the launcher peers above.
515
- '@deepseek-ai/dsh-agent': runtime,
516
- '@deepseek-ai/dsh-llm': runtime,
517
- '@deepseek-ai/dsh-scope': runtime,
518
- '@deepseek-ai/dsh-session': runtime,
519
- '@deepseek-ai/dsh-system-prompt': runtime,
520
- '@deepseek-ai/dsh-user-approval': runtime,
521
- // Capability packages that the official plugins import but declare only as peers.
522
- // A profile install leaves them out and boot then fails inside an otherwise
523
- // healthy plugin's import with ERR_MODULE_NOT_FOUND.
524
- '@deepseek-ai/dsh-attachment': runtime,
525
- '@deepseek-ai/dsh-compaction': runtime,
526
- '@deepseek-ai/dsh-fs': runtime,
527
- '@deepseek-ai/dsh-shell': runtime,
528
- '@deepseek-ai/dsh-spill': runtime,
529
- '@deepseek-ai/dsh-sandbox': runtime,
530
- '@deepseek-ai/dsh-ptc-runtime': runtime,
531
- '@deepseek-ai/dsh-llm-deepseek': runtime,
532
- '@deepseek-ai/dsh-deepseek-account': runtime,
533
- '@deepseek-ai/dsh-anonymous-user-id': runtime,
534
- '@deepseek-ai/dsh-bash-local': runtime,
535
- '@deepseek-ai/dsh-client-store': runtime,
536
- '@deepseek-ai/dsh-jobs': runtime,
537
- '@deepseek-ai/dsh-sdk-protocol': runtime,
538
- '@deepseek-ai/dsh-session-persistence': runtime,
539
- '@deepseek-ai/dsh-session-telemetry': runtime,
540
- '@deepseek-ai/dsh-session-title-llm': runtime,
541
- '@deepseek-ai/dsh-util-workspace-path': runtime,
542
- '@deepseek-ai/dsh-workflow': runtime,
543
- ...Object.fromEntries(baseBundles
544
- .filter(bundle => bundle.startsWith('@deepseek-ai/dsh-'))
545
- .map(bundle => [bundle, runtime])),
546
- };
547
- const compositionPackages = composition === undefined
548
- ? basePackages
549
- : { ...basePackages, ...composition.packages };
550
- // Official cordis rows name the historical unscoped packages. The fork ships under
551
- // `@sparkelf/*`, so the row would fail to resolve unless the profile also carries the
552
- // historical name as an alias to the same version.
553
- const rowAliases = {
554
- 'dsh-better-sidebar': '@sparkelf/dsh-better-sidebar',
555
- 'dsh-sql-workbench': '@sparkelf/dsh-sql-workbench',
556
- };
557
- for (const [alias, target] of Object.entries(rowAliases)) {
558
- const spec = compositionPackages[target];
559
- if (spec !== undefined && compositionPackages[alias] === undefined) {
560
- compositionPackages[alias] = 'npm:' + target + '@' + spec;
561
- }
562
- }
563
- const compositionBundles = composition === undefined
564
- ? baseBundles
565
- : [...baseBundles, ...composition.bundles];
613
+ const distribution = readDistributionProfile(paths.distributionDirectory, editionId, extraExcludes);
566
614
  if (existsSync(manifestPath)) {
567
- // A selected edition is an explicit install choice, so its packages and bundle order
568
- // replace the profile's rather than being ignored. Other profile decisions stay.
569
- if (composition !== undefined) {
570
- const manifest = requireRecord(JSON.parse(readFileSync(manifestPath, 'utf8')), 'Plus profile manifest');
571
- manifest.dependencies = { ...manifest.dependencies, ...compositionPackages };
572
- manifest.dsh = {
573
- ...manifest.dsh,
574
- profile: {
575
- ...manifest.dsh?.profile,
576
- bundles: [...compositionBundles],
577
- ...(edition === undefined ? {} : { plusEdition: edition }),
578
- },
579
- };
580
- writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
581
- }
582
615
  // The workspace carries decisions the distribution owns — overrides and the build
583
616
  // script allowlist — and a distribution release changes them. Rewriting on every
584
617
  // start is what lets an upgraded installation receive the new values; a profile
585
618
  // written once keeps whatever its own release decided and can never be corrected.
586
- writeProfileOverrides(paths.profileDirectory, distribution.overrides, distribution.allowBuilds, paths.omittedPackages);
587
- installProfilePackages(paths, consumerDirectory);
619
+ writeProfileOverrides(paths.profileDirectory, distribution.overrides, distribution.allowBuilds, { ...paths.omittedPackages, ...distribution.omittedPackages });
620
+ if (!dryRun) {
621
+ installProfilePackages(paths, new Set(Object.keys(distribution.omittedPackages)), distribution.runtimeVersion, registryOrder()[0]);
622
+ }
588
623
  return false;
589
624
  }
590
625
  mkdirSync(paths.profileDirectory, { recursive: true });
591
626
  const manifest = {
592
- name: 'dsh-profile-' + STANDALONE_PROFILE,
627
+ name: 'dsh-profile-' + paths.profileName,
593
628
  private: true,
594
629
  type: 'module',
595
- dependencies: compositionPackages,
630
+ dependencies: {
631
+ '@sparkelf/dsh-plus': distribution.version,
632
+ // The launcher's own tree supplies every service package the bundles mount. A
633
+ // profile that lists only the distribution's plugins installs a partial tree and
634
+ // fails at load with a module the launcher would have carried.
635
+ '@deepseek-ai/dsh': distribution.dshRange,
636
+ ...distribution.dependencies,
637
+ },
596
638
  dsh: {
597
639
  profile: {
598
- bundles: [...compositionBundles],
640
+ bundles: distribution.bundles,
599
641
  patchReload: 'live',
600
- ...(edition === undefined ? {} : { plusEdition: edition }),
601
642
  },
602
643
  },
603
644
  };
604
645
  writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
605
646
  // The overrides must reach the workspace before the install that reads them.
606
- writeProfileOverrides(paths.profileDirectory, distribution.overrides, distribution.allowBuilds, paths.omittedPackages);
607
- installProfilePackages(paths, consumerDirectory);
647
+ writeProfileOverrides(paths.profileDirectory, distribution.overrides, distribution.allowBuilds, { ...paths.omittedPackages, ...distribution.omittedPackages });
648
+ if (!dryRun) {
649
+ installProfilePackages(paths, new Set(Object.keys(distribution.omittedPackages)), distribution.runtimeVersion, registryOrder()[0]);
650
+ }
608
651
  return true;
609
652
  }
610
653
  /**
@@ -642,7 +685,7 @@ function writeProfileOverrides(profileDirectory, overrides, allowBuilds, omitted
642
685
  if (document.get('nodeLinker') === undefined)
643
686
  document.set('nodeLinker', 'hoisted');
644
687
  if (document.get('autoInstallPeers') === undefined)
645
- document.set('autoInstallPeers', true);
688
+ document.set('autoInstallPeers', false);
646
689
  writeFileSync(workspacePath, String(document));
647
690
  }
648
691
  /** Run git in one directory, returning undefined instead of throwing when asked to. */
@@ -676,8 +719,6 @@ export function applyProfileNpmPatches(distributionDirectory, profileDirectory)
676
719
  const patchSet = readPatchSet(distributionDirectory, manifest);
677
720
  const applied = [];
678
721
  for (const entry of [...patchSet.entries, ...patchSet.thirdPartyPatches]) {
679
- // Only npm targets reach an installed package; a source target needs the
680
- // official checkout, which a standalone installation does not have.
681
722
  if (entry.target.kind !== 'npm')
682
723
  continue;
683
724
  const targetName = entry.target.name;
@@ -698,22 +739,7 @@ function resolveInstalledPackage(from, packageName) {
698
739
  manifestPath = requireFrom.resolve(packageName + '/package.json');
699
740
  }
700
741
  catch {
701
- // Some packages do not export `./package.json`; resolve the entry module instead and walk
702
- // up to the directory that holds the manifest.
703
- let directory;
704
- try {
705
- directory = dirname(requireFrom.resolve(packageName));
706
- }
707
- catch {
708
- throw new Error(packageName + ' is not installed under ' + from);
709
- }
710
- while (!existsSync(resolve(directory, 'package.json'))) {
711
- const parent = dirname(directory);
712
- if (parent === directory)
713
- throw new Error(packageName + ' has no resolvable package manifest under ' + from);
714
- directory = parent;
715
- }
716
- manifestPath = resolve(directory, 'package.json');
742
+ throw new Error(packageName + ' is not installed under ' + from);
717
743
  }
718
744
  const manifest = requireRecord(JSON.parse(readFileSync(manifestPath, 'utf8')), packageName + ' manifest');
719
745
  return { name: packageName, version: String(manifest.version), directory: dirname(manifestPath), manifest };