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

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 +229 -47
  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 +93 -2
  12. package/lib/types/standalone-profile.d.ts +8 -1
  13. package/lib/types/standalone-profile.js +191 -53
  14. package/package.json +603 -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
@@ -9,11 +9,13 @@
9
9
  * bundle's own list.
10
10
  */
11
11
  import { spawnSync } from 'node:child_process';
12
- import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, symlinkSync, writeFileSync } from 'node:fs';
12
+ import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
13
13
  import { createRequire } from 'node:module';
14
+ import { fileURLToPath } from 'node:url';
14
15
  import { homedir } from 'node:os';
15
16
  import { dirname, join, posix, resolve, win32 } from 'node:path';
16
17
  import { parseDocument } from 'yaml';
18
+ import { readPatchSet } from "./patch-set.js";
17
19
  /** Profile name a standalone installation owns when its package declares none. */
18
20
  export const STANDALONE_PROFILE = 'plus';
19
21
  /**
@@ -97,6 +99,12 @@ function requireStringArray(value, label) {
97
99
  throw new Error(label + ' must be a string array');
98
100
  return value;
99
101
  }
102
+ /** Read one non-empty string field. */
103
+ function requireString(value, label) {
104
+ if (typeof value !== 'string' || value.trim() === '')
105
+ throw new Error(label + ' must be a non-empty string');
106
+ return value;
107
+ }
100
108
  /** Resolve the DSH home, honouring the environment override the launcher itself reads. */
101
109
  export function resolveHome(env = process.env) {
102
110
  const configured = env.DSH_HOME;
@@ -135,12 +143,15 @@ export function isWithin(candidate, parent, platform = process.platform) {
135
143
  * @returns absolute path to the installed distribution.
136
144
  */
137
145
  export function resolveDistributionDirectory(anchor) {
146
+ const ownDirectory = dirname(dirname(fileURLToPath(import.meta.url)));
138
147
  let resolved;
139
148
  try {
140
149
  resolved = dirname(createRequire(anchor).resolve('@sparkelf/dsh-plus/package.json'));
141
150
  }
142
151
  catch {
143
- throw new Error('@sparkelf/dsh-plus is not installed in ' + dirname(anchor) + '; run this command from the directory that installed it');
152
+ // `npx @sparkelf/dsh-plus ...` runs the CLI from a temporary install, so the consumer
153
+ // tree has no copy to resolve. The running module's own package is the distribution.
154
+ return ownDirectory;
144
155
  }
145
156
  // A resolution that leaves the consumer tree means the module answered from its own
146
157
  // installation, which reports health for a distribution the consumer never installed.
@@ -156,21 +167,14 @@ export function resolveDistributionDirectory(anchor) {
156
167
  break;
157
168
  current = parent;
158
169
  }
159
- throw new Error('@sparkelf/dsh-plus resolved outside ' + resolve(dirname(anchor)) + ' (found ' + resolved + '); run this command from the directory that installed it');
170
+ return ownDirectory;
160
171
  }
161
- /** The reviewed bundle order and pins the installed distribution declares. */
162
172
  export function readDistributionProfile(distributionDirectory) {
163
173
  const manifest = requireRecord(JSON.parse(readFileSync(join(distributionDirectory, 'package.json'), 'utf8')), 'Plus distribution manifest');
164
174
  const plus = requireRecord(manifest.dshPlus, 'dshPlus');
165
175
  const profile = requireRecord(plus.profile, 'dshPlus.profile');
166
- const rawDependencies = requireRecord(profile.dependencies, 'dshPlus.profile.dependencies');
176
+ const dependencies = requireDependencyMap(requireRecord(profile.dependencies, 'dshPlus.profile.dependencies'), 'dshPlus.profile.dependencies');
167
177
  const rawAllowBuilds = requireRecord(profile.allowBuilds, 'dshPlus.profile.allowBuilds');
168
- const dependencies = {};
169
- for (const [name, spec] of Object.entries(rawDependencies)) {
170
- if (typeof spec !== 'string' || spec === '')
171
- throw new Error('dshPlus.profile.dependencies.' + name + ' must be a non-empty string');
172
- dependencies[name] = spec;
173
- }
174
178
  const allowBuilds = {};
175
179
  for (const [name, allowed] of Object.entries(rawAllowBuilds)) {
176
180
  if (typeof allowed !== 'boolean')
@@ -189,6 +193,16 @@ export function readDistributionProfile(distributionDirectory) {
189
193
  throw new Error('dshPlus.profile.overrides.' + name + ' must be a non-empty string');
190
194
  overrides[name] = spec;
191
195
  }
196
+ const editions = {};
197
+ const rawEditions = plus.editions === undefined ? {} : requireRecord(plus.editions, 'dshPlus.editions');
198
+ for (const [id, value] of Object.entries(rawEditions)) {
199
+ const edition = requireRecord(value, 'dshPlus.editions.' + id);
200
+ editions[id] = {
201
+ label: requireString(edition.label, 'dshPlus.editions.' + id + '.label'),
202
+ bundles: requireStringArray(edition.bundles, 'dshPlus.editions.' + id + '.bundles'),
203
+ packages: requireDependencyMap(requireRecord(edition.packages, 'dshPlus.editions.' + id + '.packages'), 'dshPlus.editions.' + id + '.packages'),
204
+ };
205
+ }
192
206
  const compatibility = requireRecord(plus.compatibility, 'dshPlus.compatibility');
193
207
  return {
194
208
  name: String(manifest.name),
@@ -196,10 +210,21 @@ export function readDistributionProfile(distributionDirectory) {
196
210
  dependencies,
197
211
  allowBuilds,
198
212
  overrides,
213
+ editions,
199
214
  dshRange: String(compatibility.dsh),
200
215
  version: String(manifest.version),
201
216
  };
202
217
  }
218
+ /** Read one package-name-to-spec map, rejecting empty entries. */
219
+ function requireDependencyMap(value, label) {
220
+ const dependencies = {};
221
+ for (const [name, spec] of Object.entries(value)) {
222
+ if (typeof spec !== 'string' || spec === '')
223
+ throw new Error(label + '.' + name + ' must be a non-empty string');
224
+ dependencies[name] = spec;
225
+ }
226
+ return dependencies;
227
+ }
203
228
  /**
204
229
  * Give the profile its own installed tree, built from the distribution's declarations.
205
230
  *
@@ -221,7 +246,12 @@ export function readDistributionProfile(distributionDirectory) {
221
246
  export function installProfilePackages(paths, consumerDirectory) {
222
247
  const consumerModules = join(consumerDirectory, 'node_modules');
223
248
  if (!existsSync(consumerModules)) {
224
- throw new Error('no node_modules in ' + consumerDirectory + '; run npm install there first');
249
+ // `npx @sparkelf/dsh-plus` runs from a temporary package tree that has no launcher
250
+ // beside it. The profile installs its own tree, so there is nothing to link from
251
+ // the caller; only the profile's packages matter.
252
+ installWithRegistryFallback(paths);
253
+ alignReplacedPackageNames(join(paths.profileDirectory, 'node_modules'));
254
+ return;
225
255
  }
226
256
  const profileModules = join(paths.profileDirectory, 'node_modules');
227
257
  if (existsSync(profileModules)) {
@@ -246,10 +276,18 @@ export function installProfilePackages(paths, consumerDirectory) {
246
276
  function installWithRegistryFallback(paths) {
247
277
  const registries = registryOrder();
248
278
  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 });
249
284
  const result = spawnSync('pnpm', ['install', '--no-frozen-lockfile', '--registry', registry], {
250
285
  cwd: paths.profileDirectory,
251
286
  stdio: 'inherit',
252
287
  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,
253
291
  });
254
292
  if (result.error !== undefined)
255
293
  throw result.error;
@@ -312,8 +350,12 @@ const MAINLAND_REGISTRY = 'https://registry.npmmirror.com';
312
350
  */
313
351
  export function registryOrder() {
314
352
  const configured = process.env.DSH_PLUS_INSTALL_REGISTRY;
315
- if (configured !== undefined && configured !== '')
316
- return [configured, MAINLAND_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
+ }
317
359
  // Intl reports the system locale on every platform. The POSIX variables are empty on
318
360
  // Windows, so reading them alone classified every Windows console as non-mainland and
319
361
  // reached the origin first — measured in a consumer's log, which showed npmjs failing
@@ -430,12 +472,113 @@ export function resolvePaths(anchor, env = process.env) {
430
472
  *
431
473
  * @param paths - resolved standalone paths.
432
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.
433
476
  * @returns whether this call created the manifest.
434
477
  */
435
- export function ensureProfile(paths, consumerDirectory) {
478
+ export function ensureProfile(paths, consumerDirectory, edition) {
436
479
  const manifestPath = join(paths.profileDirectory, 'package.json');
437
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];
438
566
  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
+ }
439
582
  // The workspace carries decisions the distribution owns — overrides and the build
440
583
  // script allowlist — and a distribution release changes them. Rewriting on every
441
584
  // start is what lets an upgraded installation receive the new values; a profile
@@ -449,18 +592,12 @@ export function ensureProfile(paths, consumerDirectory) {
449
592
  name: 'dsh-profile-' + STANDALONE_PROFILE,
450
593
  private: true,
451
594
  type: 'module',
452
- dependencies: {
453
- '@sparkelf/dsh-plus': distribution.version,
454
- // The launcher's own tree supplies every service package the bundles mount. A
455
- // profile that lists only the distribution's plugins installs a partial tree and
456
- // fails at load with a module the launcher would have carried.
457
- '@deepseek-ai/dsh': distribution.dshRange,
458
- ...distribution.dependencies,
459
- },
595
+ dependencies: compositionPackages,
460
596
  dsh: {
461
597
  profile: {
462
- bundles: distribution.bundles,
598
+ bundles: [...compositionBundles],
463
599
  patchReload: 'live',
600
+ ...(edition === undefined ? {} : { plusEdition: edition }),
464
601
  },
465
602
  },
466
603
  };
@@ -505,7 +642,7 @@ function writeProfileOverrides(profileDirectory, overrides, allowBuilds, omitted
505
642
  if (document.get('nodeLinker') === undefined)
506
643
  document.set('nodeLinker', 'hoisted');
507
644
  if (document.get('autoInstallPeers') === undefined)
508
- document.set('autoInstallPeers', false);
645
+ document.set('autoInstallPeers', true);
509
646
  writeFileSync(workspacePath, String(document));
510
647
  }
511
648
  /** Run git in one directory, returning undefined instead of throwing when asked to. */
@@ -536,34 +673,20 @@ function git(root, args, acceptFailure = false) {
536
673
  */
537
674
  export function applyProfileNpmPatches(distributionDirectory, profileDirectory) {
538
675
  const manifest = requireRecord(JSON.parse(readFileSync(join(distributionDirectory, 'package.json'), 'utf8')), 'Plus distribution manifest');
539
- const plus = requireRecord(manifest.dshPlus, 'dshPlus');
540
- const names = plus.patchPackages;
541
- if (!Array.isArray(names))
542
- throw new Error('dshPlus.patchPackages must be an array');
676
+ const patchSet = readPatchSet(distributionDirectory, manifest);
543
677
  const applied = [];
544
- for (const value of names) {
545
- if (typeof value !== 'string' || value === '')
546
- throw new Error('dshPlus.patchPackages entries must be non-empty strings');
547
- const patchPackage = resolveInstalledPackage(distributionDirectory, value);
548
- const declaration = requireRecord(patchPackage.manifest.dshPatch, value + ' dshPatch');
549
- const variants = declaration.variants;
550
- if (!Array.isArray(variants))
551
- throw new Error(value + ' dshPatch.variants must be an array');
552
- for (const entry of variants) {
553
- const variant = requireRecord(entry, value + ' variant');
554
- const target = requireRecord(variant.target, value + ' variant target');
555
- // Only npm targets reach an installed package; a source target needs the
556
- // official checkout, which a standalone installation does not have.
557
- if (target.kind !== 'npm')
558
- continue;
559
- const targetName = String(target.name);
560
- const patched = resolveInstalledPackage(profileDirectory, targetName);
561
- const file = resolve(patchPackage.directory, String(variant.file));
562
- if (git(patched.directory, ['apply', '--reverse', '--check', file], true) !== undefined)
563
- continue;
564
- git(patched.directory, ['apply', file]);
565
- applied.push(value + ' -> ' + targetName);
566
- }
678
+ 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
+ if (entry.target.kind !== 'npm')
682
+ continue;
683
+ const targetName = entry.target.name;
684
+ const patched = resolveInstalledPackage(profileDirectory, targetName);
685
+ const file = resolve(distributionDirectory, entry.file);
686
+ if (git(patched.directory, ['apply', '--reverse', '--check', file], true) !== undefined)
687
+ continue;
688
+ git(patched.directory, ['apply', file]);
689
+ applied.push(entry.id + ' -> ' + targetName);
567
690
  }
568
691
  return applied;
569
692
  }
@@ -575,7 +698,22 @@ function resolveInstalledPackage(from, packageName) {
575
698
  manifestPath = requireFrom.resolve(packageName + '/package.json');
576
699
  }
577
700
  catch {
578
- throw new Error(packageName + ' is not installed under ' + from);
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');
579
717
  }
580
718
  const manifest = requireRecord(JSON.parse(readFileSync(manifestPath, 'utf8')), packageName + ' manifest');
581
719
  return { name: packageName, version: String(manifest.version), directory: dirname(manifestPath), manifest };