@deepseek-ai/dsh-plugin-manager 0.2.0-rc.2 → 0.2.1-alpha.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.
@@ -42,14 +42,15 @@ import { withFileLock, writeFileAtomic } from '@deepseek-ai/dsh-atomic-write';
42
42
  import z from '@deepseek-ai/schemastery';
43
43
  import { TypertRemoteService, Remote } from '@deepseek-ai/dsh-typert-protocol';
44
44
  import { pluginEntryId, readPluginInventory } from '@deepseek-ai/dsh-host-plugin-inventory';
45
- import { readPluginMeta, readProfileManifest, resolveBundleDir, loadOverlayPatches, composeEntries, reconcileProfilePatches, readProfilePatches, OPTIONAL_BUNDLES, bundlePatchPaths, evaluatePluginCompatibility, readProfileCompatibility, readProfileVersionExemptions, setProfileVersionExemption, PROFILE_COMPATIBILITY_FILENAME, } from '@deepseek-ai/dsh-app-boot';
45
+ import { readPluginMeta, readProfileManifest, resolveBundleDir, loadOverlayPatches, composeEntries, reconcileProfilePatches, readProfilePatches, OPTIONAL_BUNDLES, bundlePatchPaths, evaluatePluginCompatibility, readProfileCompatibility, readProfileVersionExemptions, setProfileVersionExemption, PROFILE_COMPATIBILITY_FILENAME, OFFICIAL_ON_DEMAND_CATALOG, getDshRuntimeVersion, } from '@deepseek-ai/dsh-app-boot';
46
46
  import { bundleManifest, readProfileRegistry, registryArguments, runProfilePnpm, saveManifest, viewProfilePackage } from "./operations.js";
47
47
  import { classifyInstallFailure } from "./install-failure.js";
48
- import { InvalidInstallSpecError, parseInstallSpec } from "./install-spec.js";
48
+ import { officialBundleInstallTarget } from "./official-install-target.js";
49
+ import { dependencySpec, InvalidInstallSpecError, parseInstallSpec } from "./install-spec.js";
49
50
  import { attributeFailure, normalizeRegistry, NPMMIRROR_REGISTRY, registryPlan } from "./registry.js";
50
51
  import { writePluginEnabled } from "./patch.js";
51
52
  import { incompatiblePlugin, ManagementFailure } from "./failure.js";
52
- import { approveBuilds, readPendingBuilds } from "./build-approval.js";
53
+ import { approveBuilds, recordPendingBuilds } from "./build-approval.js";
53
54
  import { checkGithubConnection } from "./github-connection.js";
54
55
  export { classifyInstallFailure } from "./install-failure.js";
55
56
  export { InvalidInstallSpecError, parseInstallSpec } from "./install-spec.js";
@@ -274,49 +275,83 @@ let PluginManager = (() => {
274
275
  return { ...entry, patchId: candidate.id };
275
276
  });
276
277
  }
277
- /** Read the profile's installed bundles, the bundles this dsh installation supplies, and the selected names that are not bundles.
278
+ /** Read installed, installation-provided, and offline Official catalog bundles, plus selected non-bundle names.
278
279
  * A dependency without a bundle patch is listed, as a `not-bundle` problem, only while it is selected.
279
- * @returns Package versions, manifest descriptions, rows, optional display metadata, activation selections,
280
- * whether the installation offers the bundle, and removal availability.
280
+ * @returns Package versions, manifest descriptions, the installable spec of profile dependencies, rows, optional
281
+ * display metadata, activation selections, whether the installation offers the bundle, and removal availability.
281
282
  */
282
283
  listBundles() {
283
284
  const manifest = readProfileManifest('dsh', this.profile.dir);
284
285
  const exemptions = readProfileVersionExemptions(this.profile.dir);
285
286
  const selected = manifest.dsh?.profile?.bundles ?? [];
286
- const dependencies = Object.keys(manifest.dependencies ?? {});
287
+ const recorded = manifest.dependencies ?? {};
288
+ const dependencies = Object.keys(recorded);
287
289
  const installation = JSON.parse(readFileSync(this.profile.installAnchor, 'utf8'));
288
- const names = [...new Set([...selected, ...dependencies, ...Object.keys(installation.dependencies ?? {})])];
290
+ const catalog = new Map(OFFICIAL_ON_DEMAND_CATALOG.map(entry => [entry.packageName, entry]));
291
+ const names = [...new Set([...selected, ...dependencies, ...Object.keys(installation.dependencies ?? {}), ...catalog.keys()])];
289
292
  const bundles = [];
293
+ const runtimeVersion = getDshRuntimeVersion();
290
294
  for (const name of names) {
291
295
  const installed = dependencies.includes(name);
292
296
  const optional = OPTIONAL_BUNDLES.includes(name);
293
- const removable = installed && !Object.hasOwn(installation.dependencies ?? {}, name);
294
297
  const enabled = selected.includes(name);
298
+ const shipped = Object.hasOwn(installation.dependencies ?? {}, name);
299
+ const offered = catalog.get(name);
300
+ const official = optional || offered !== undefined;
301
+ let target = {};
302
+ let targetError;
303
+ if (offered !== undefined) {
304
+ try {
305
+ target = { installTarget: officialBundleInstallTarget(name, this.profile.installAnchor, runtimeVersion) };
306
+ }
307
+ catch (error) {
308
+ targetError = managementError(error);
309
+ }
310
+ }
311
+ const catalogMeta = offered === undefined ? {} : { meta: offered.meta };
312
+ if (offered !== undefined && !installed && !shipped) {
313
+ bundles.push({ name, official, availability: 'missing', ...catalogMeta, ...target, enabled, installed, optional,
314
+ removable: enabled, rows: [], overrides: [],
315
+ ...targetError !== undefined ? { error: targetError } : enabled ? { error: { code: 'not-bundle' } } : {} });
316
+ continue;
317
+ }
318
+ // Installation-owned copies remain protected even if a profile also declares their names.
319
+ const owned = installed && !shipped;
320
+ const removable = owned || (enabled && !installed && !shipped);
321
+ const sourceOf = (packageName) => owned ? { source: dependencySpec(name, recorded[name], this.profile.dir, packageName) } : {};
295
322
  const readOnlyReason = this.protectsManager(name) ? 'management-required' : undefined;
323
+ let availability = 'missing';
324
+ let version;
296
325
  try {
297
326
  const info = bundleManifest(name, this.profile.dir, this.profile.installAnchor);
327
+ availability = shipped ? 'installation' : 'profile';
298
328
  if (info === undefined) {
299
- if (enabled)
300
- bundles.push({ name, enabled, installed, optional, removable: removable && readOnlyReason === undefined,
301
- ...(readOnlyReason === undefined ? {} : { readOnlyReason }), error: { code: 'not-bundle' }, rows: [], overrides: [] });
329
+ if (enabled || offered !== undefined)
330
+ bundles.push({ name, official, availability, ...catalogMeta, ...target,
331
+ ...sourceOf(), enabled, installed, optional, removable: removable && readOnlyReason === undefined,
332
+ ...(readOnlyReason === undefined ? {} : { readOnlyReason }), error: targetError ?? { code: 'not-bundle' }, rows: [], overrides: [] });
302
333
  continue;
303
334
  }
335
+ version = info.version;
304
336
  const compatibility = evaluatePluginCompatibility(info, exemptions);
305
337
  if (compatibility !== undefined && !compatibility.exempted)
306
338
  throw new ManagementFailure('incompatible-version', [incompatiblePlugin(compatibility)]);
307
339
  const dir = resolveBundleDir('dsh', name, this.profile.installAnchor, this.profile.dir);
308
- const meta = readPluginMeta(info.name ?? name, pathToFileURL(join(dir, 'package.json')).href);
309
- bundles.push({ name, ...(info.version === undefined ? {} : { version: info.version }),
340
+ const meta = offered?.meta ?? readPluginMeta(info.name ?? name, pathToFileURL(join(dir, 'package.json')).href);
341
+ bundles.push({ name, official, availability, ...target, ...(version === undefined ? {} : { version }),
310
342
  ...(info.description === undefined || info.description === '' ? {} : { description: info.description }),
311
343
  ...meta === undefined ? {} : { meta },
312
- enabled, installed, optional, removable: removable && readOnlyReason === undefined,
344
+ ...sourceOf(info.name), enabled, installed, optional, removable: removable && readOnlyReason === undefined,
313
345
  ...(readOnlyReason === undefined ? {} : { readOnlyReason }),
314
- ...this.declaredRows(name, info) });
346
+ ...this.declaredRows(name, info),
347
+ ...targetError === undefined ? {} : { error: targetError } });
315
348
  }
316
349
  catch (error) {
317
- if (enabled || installed) {
318
- bundles.push({ name, enabled, installed, optional, removable: removable && readOnlyReason === undefined,
319
- ...(readOnlyReason === undefined ? {} : { readOnlyReason }), error: managementError(error), rows: [], overrides: [] });
350
+ if (enabled || installed || optional || offered !== undefined) {
351
+ bundles.push({ name, official, availability, ...catalogMeta, ...target, ...(version === undefined ? {} : { version }),
352
+ ...sourceOf(), enabled, installed, optional, removable: removable && readOnlyReason === undefined,
353
+ ...(readOnlyReason === undefined ? {} : { readOnlyReason }),
354
+ error: targetError ?? managementError(error), rows: [], overrides: [] });
320
355
  }
321
356
  }
322
357
  }
@@ -456,7 +491,11 @@ let PluginManager = (() => {
456
491
  setBundleEnabled(name, enabled) {
457
492
  return this.change(result => this.configure(async () => {
458
493
  await this.selectBundle(name, enabled);
494
+ if (enabled)
495
+ await this.refreshPackages();
459
496
  result.warnings = await this.reload(enabled ? this.bundleRows(name).map(row => row.id) : []);
497
+ if (!enabled && this.ownerContext.get('hmr') !== undefined)
498
+ await this.refreshPackages();
460
499
  }), { stage: 'enable', target: name, enabled }, 'bundle');
461
500
  }
462
501
  /**
@@ -467,7 +506,7 @@ let PluginManager = (() => {
467
506
  * `package.json` and `pnpm-lock.yaml` as they were; downloaded files can stay.
468
507
  * @param spec One package spec, including local paths relative to the invocation directory.
469
508
  * @param options Whether to activate the installed bundle (defaults to true), the request id a cancellation names,
470
- * the pending build scripts to allow for this profile before pnpm runs, and the registry asked first.
509
+ * the pending build scripts to allow, whether to save an exact dependency, and the registry asked first.
471
510
  * @returns Package-manager diagnostics, the registries asked, and the observed activation outcome.
472
511
  */
473
512
  installBundle(spec, options) {
@@ -492,6 +531,7 @@ let PluginManager = (() => {
492
531
  const files = await this.readRestoredFiles();
493
532
  const before = readProfileManifest('dsh', this.profile.dir).dependencies ?? {};
494
533
  let name;
534
+ let version;
495
535
  try {
496
536
  result.registries = [];
497
537
  const connection = checkGithubConnection(parsedForRegistry(spec), this.profile.dir, {
@@ -525,7 +565,7 @@ let PluginManager = (() => {
525
565
  throw new InstallCancelledError();
526
566
  result.registries.push(registry);
527
567
  announce('installing', { registry, index: index + 1, total: plan.length });
528
- run = await this.runPnpm(['add', spec, ...registryArguments(registry)], control.abort.signal, requestId);
568
+ run = await this.runPnpm(['add', spec, ...options?.saveExact === true ? ['--save-exact'] : [], ...registryArguments(registry)], control.abort.signal, requestId);
529
569
  result.packageResult = run;
530
570
  if (stopped())
531
571
  throw new InstallCancelledError();
@@ -557,9 +597,9 @@ let PluginManager = (() => {
557
597
  if (succeeded)
558
598
  delete result.failedAt;
559
599
  if (!succeeded) {
560
- // pnpm-workspace.yaml is not restored, so the names pnpm left undecided there can be offered for approval.
600
+ // pnpm-workspace.yaml is not restored, so the names it left undecided, or reported as ignored, are offered for approval.
561
601
  try {
562
- result.pendingBuilds = await readPendingBuilds(this.profile.dir);
602
+ result.pendingBuilds = await recordPendingBuilds(this.profile.dir, run.output);
563
603
  }
564
604
  catch (error) {
565
605
  this.ownerContext.logger.warn('Could not read pending build approvals after pnpm failed', error);
@@ -569,8 +609,11 @@ let PluginManager = (() => {
569
609
  const after = readProfileManifest('dsh', this.profile.dir).dependencies ?? {};
570
610
  const installed = Object.keys(after).filter(name => before[name] !== after[name]);
571
611
  // Registry retries can retain the saved range after a partial installation.
572
- if (installed.length === 0)
573
- installed.push(...Object.keys(after).filter(name => spec === name || spec.startsWith(`${name}@`)));
612
+ if (installed.length === 0) {
613
+ const parsed = parsedForRegistry(spec);
614
+ const localName = parsed.kind === 'path' ? readProfileManifest('dsh', parsed.path).name : undefined;
615
+ installed.push(...Object.keys(after).filter(name => name === localName || spec === name || spec.startsWith(`${name}@`)));
616
+ }
574
617
  const target = installed[0];
575
618
  if (installed.length !== 1 || target === undefined)
576
619
  throw new ManagementFailure('ambiguous-install');
@@ -584,6 +627,7 @@ let PluginManager = (() => {
584
627
  throw new ManagementFailure('incompatible-version', [incompatiblePlugin(compatibility)]);
585
628
  for (const file of bundlePatchPaths(dir, manifest.dsh.bundle))
586
629
  loadOverlayPatches('dsh', file);
630
+ version = manifest.version;
587
631
  }
588
632
  catch (error) {
589
633
  // pnpm has exited by now, so the files it rewrote go back as they were.
@@ -593,6 +637,8 @@ let PluginManager = (() => {
593
637
  control.phase = 'applying';
594
638
  announce('applying');
595
639
  result.bundle = name;
640
+ if (version !== undefined)
641
+ result.version = version;
596
642
  result.target = name;
597
643
  result.stage = 'enable';
598
644
  return this.configure(async () => {
@@ -600,6 +646,7 @@ let PluginManager = (() => {
600
646
  await this.selectBundle(name, true);
601
647
  if (Object.hasOwn(before, name))
602
648
  return 'restart-required';
649
+ await this.refreshPackages();
603
650
  if (options?.enabled !== false)
604
651
  result.warnings = await this.reload();
605
652
  });
@@ -633,13 +680,14 @@ let PluginManager = (() => {
633
680
  await control.result.then(() => undefined, () => undefined);
634
681
  return { status: 'cancelled' };
635
682
  }
636
- /** Unload and remove a profile-owned bundle dependency through dsh plugin's pnpm path.
637
- * @param name Installed dependency name.
683
+ /** Unload and remove a profile-owned bundle dependency through dsh plugin's pnpm path; a selected name no
684
+ * dependency holds is only deselected.
685
+ * @param name Installed dependency or selected bundle name.
638
686
  * @returns Removal diagnostics and the remaining profile state.
639
687
  */
640
688
  removeBundle(name) {
641
689
  return this.change(async (result) => {
642
- await this.configure(async () => {
690
+ const installed = await this.configure(async () => {
643
691
  const bundle = (await this.listBundles()).find(item => item.name === name);
644
692
  if (bundle === undefined || !bundle.removable)
645
693
  throw new ManagementFailure('not-removable');
@@ -657,11 +705,15 @@ let PluginManager = (() => {
657
705
  && contributions.some(row => row.id === entry.options.id && row.name === entry.options.name))) {
658
706
  throw new ManagementFailure('bundle-in-use');
659
707
  }
708
+ return bundle.installed;
660
709
  });
710
+ if (!installed)
711
+ return;
661
712
  result.packageResult = await this.runPnpm(['remove', name]);
662
713
  if (result.packageResult.exitCode !== 0 || result.packageResult.timedOut === true) {
663
714
  throw new Error(result.packageResult.output);
664
715
  }
716
+ await this.configure(() => this.refreshPackages());
665
717
  }, { stage: 'remove', target: name }, 'remove');
666
718
  }
667
719
  /** The rows a bundle's patch inserts and the existing rows it changes; an unreadable patch throws. */
@@ -684,7 +736,8 @@ let PluginManager = (() => {
684
736
  const rows = [];
685
737
  const packages = this.ctx.get('pluginPackages');
686
738
  for (const row of flatten(composeEntries([patches.filter(item => item.insert !== undefined)]))) {
687
- if (typeof row.id !== 'string' || typeof row.name !== 'string')
739
+ // Group rows are containers without a plugin inventory entry; their children carry the switches.
740
+ if (typeof row.id !== 'string' || typeof row.name !== 'string' || row.group === true)
688
741
  continue;
689
742
  const active = live.get(row.id);
690
743
  const entryId = active?.entryId;
@@ -806,6 +859,15 @@ let PluginManager = (() => {
806
859
  const apply = () => { this.abort.signal.throwIfAborted(); return operation(); };
807
860
  return hmr === undefined ? apply() : hmr.runExclusive(apply);
808
861
  }
862
+ async refreshPackages() {
863
+ if (this.ownerContext.get('hmr') === undefined) {
864
+ const selected = readProfileManifest('dsh', this.profile.dir).dsh?.profile?.bundles ?? [];
865
+ // Deselected startup bundles still run without HMR and need the existing package table.
866
+ if (this.profile.startedBundles.some(name => !selected.includes(name)))
867
+ return;
868
+ }
869
+ await this.ownerContext.get('pluginPackages')?.refresh();
870
+ }
809
871
  async reload(requiredIds = []) {
810
872
  if (this.ownerContext.get('hmr') === undefined)
811
873
  return [];
@@ -47,4 +47,17 @@ export declare class InvalidInstallSpecError extends Error {
47
47
  * registry would refuse, or a URL that is neither a git host nor a tarball.
48
48
  */
49
49
  export declare function parseInstallSpec(raw: string): ParsedInstallSpec;
50
+ /**
51
+ * The spec `pnpm add` accepts for a dependency the profile manifest records. A git address, URL, or other
52
+ * non-registry protocol is the recorded value without the user information of an http(s) URL, whose query string
53
+ * is kept; a `file:` or `link:` path, which pnpm records relative to the profile or with `~` for the home
54
+ * directory, becomes absolute. A registry range, tag, or alias follows the dependency name after `@`, as does a
55
+ * non-registry value installed under a name other than the package's own.
56
+ * @param name - the dependency name.
57
+ * @param recorded - the value the profile manifest records for it.
58
+ * @param profileDir - the profile directory a relative local path is resolved against.
59
+ * @param packageName - the name the installed package's manifest declares, when it could be read.
60
+ * @returns the installable spec.
61
+ */
62
+ export declare function dependencySpec(name: string, recorded: string, profileDir: string, packageName?: string): string;
50
63
  //# sourceMappingURL=install-spec.d.ts.map
@@ -3,7 +3,8 @@
3
3
  * takes, and for a registry name whether it is one the registry can accept.
4
4
  * @module @deepseek-ai/dsh-plugin-manager/install-spec
5
5
  */
6
- import { isAbsolute } from 'node:path';
6
+ import { homedir } from 'node:os';
7
+ import { isAbsolute, resolve } from 'node:path';
7
8
  /** The forms pnpm resolves through a git host: a host shorthand, a git URL, or a hosted repository URL. */
8
9
  const GIT_SHORTHAND = /^(?:github|gitlab|bitbucket|gist):/i;
9
10
  const GIT_URL = /^git(?:\+[a-z]+)?:\/\/|^git@[^:]+:/i;
@@ -84,4 +85,35 @@ export function parseInstallSpec(raw) {
84
85
  throw invalid(spec, 'a version after @ must not be empty');
85
86
  return range === undefined ? { kind: 'registry', spec, name } : { kind: 'registry', spec, name, range };
86
87
  }
88
+ /** A recorded dependency value pnpm resolves outside a registry: a protocol other than a registry alias, or an scp-like git address. */
89
+ const NON_REGISTRY_VALUE = /^(?!(?:npm|jsr|workspace|catalog):)[a-z][a-z0-9+.-]*:|^[^@/:\s]+@[^:/\s]+:/i;
90
+ /** The user information of an http(s) URL, up to the last `@` before the path, which holds a password or access token when present. */
91
+ const HTTP_USER_INFO = /^((?:git\+)?https?:\/\/)[^/]*@/i;
92
+ /**
93
+ * The spec `pnpm add` accepts for a dependency the profile manifest records. A git address, URL, or other
94
+ * non-registry protocol is the recorded value without the user information of an http(s) URL, whose query string
95
+ * is kept; a `file:` or `link:` path, which pnpm records relative to the profile or with `~` for the home
96
+ * directory, becomes absolute. A registry range, tag, or alias follows the dependency name after `@`, as does a
97
+ * non-registry value installed under a name other than the package's own.
98
+ * @param name - the dependency name.
99
+ * @param recorded - the value the profile manifest records for it.
100
+ * @param profileDir - the profile directory a relative local path is resolved against.
101
+ * @param packageName - the name the installed package's manifest declares, when it could be read.
102
+ * @returns the installable spec.
103
+ */
104
+ export function dependencySpec(name, recorded, profileDir, packageName = name) {
105
+ const local = /^(file|link):(.*)$/s.exec(recorded);
106
+ let spec;
107
+ if (local !== null) {
108
+ const path = local[2].replace(/^~(?=$|[\\/])/, () => homedir());
109
+ spec = `${local[1]}:${resolve(profileDir, path)}`;
110
+ }
111
+ else if (NON_REGISTRY_VALUE.test(recorded)) {
112
+ spec = recorded.replace(HTTP_USER_INFO, '$1');
113
+ }
114
+ else {
115
+ return `${name}@${recorded}`;
116
+ }
117
+ return packageName === name ? spec : `${name}@${spec}`;
118
+ }
87
119
  //# sourceMappingURL=install-spec.js.map
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Offer a source checkout link or an exact published package version. Source installations are
3
+ * identified by the real CLI manifest and its unpacked workspace dependencies, including Desktop's
4
+ * development symlink. An incomplete checkout is an error, never a registry fallback.
5
+ * @param name - Official catalog package name.
6
+ * @param installAnchor - Running application's DSH package.json, possibly reached through a symlink.
7
+ * @param version - Running DSH version used by its release family.
8
+ * @returns An ordinary package-manager spec and its package version.
9
+ * @throws when a source installation lacks its workspace package, bundle patch, or built runtime.
10
+ */
11
+ export declare function officialBundleInstallTarget(name: string, installAnchor: string, version: string): {
12
+ spec: string;
13
+ version: string;
14
+ };
15
+ //# sourceMappingURL=official-install-target.d.ts.map
@@ -0,0 +1,54 @@
1
+ /** Select Official bundle packages from the running installation without importing provider code. */
2
+ import { existsSync, globSync, readFileSync, realpathSync, statSync } from 'node:fs';
3
+ import { dirname, join, resolve } from 'node:path';
4
+ import { bundlePatchPaths } from '@deepseek-ai/dsh-app-boot';
5
+ function manifestAt(file) {
6
+ return JSON.parse(readFileSync(file, 'utf8'));
7
+ }
8
+ /**
9
+ * Offer a source checkout link or an exact published package version. Source installations are
10
+ * identified by the real CLI manifest and its unpacked workspace dependencies, including Desktop's
11
+ * development symlink. An incomplete checkout is an error, never a registry fallback.
12
+ * @param name - Official catalog package name.
13
+ * @param installAnchor - Running application's DSH package.json, possibly reached through a symlink.
14
+ * @param version - Running DSH version used by its release family.
15
+ * @returns An ordinary package-manager spec and its package version.
16
+ * @throws when a source installation lacks its workspace package, bundle patch, or built runtime.
17
+ */
18
+ export function officialBundleInstallTarget(name, installAnchor, version) {
19
+ const anchor = realpathSync(installAnchor);
20
+ const installation = manifestAt(anchor);
21
+ const source = installation.name === '@deepseek-ai/dsh'
22
+ && Object.values(installation.dependencies ?? {}).some(value => value.startsWith('workspace:'));
23
+ if (!source)
24
+ return { spec: `${name}@${version}`, version };
25
+ const root = resolve(dirname(anchor), '../..');
26
+ if (anchor !== join(root, 'apps', 'cli', 'package.json')
27
+ || !existsSync(join(root, 'package.json'))
28
+ || manifestAt(join(root, 'package.json')).name !== '@deepseek-ai/dsh-root') {
29
+ throw new Error(`Official bundle ${name}: source DSH installation ${anchor} requires its complete workspace checkout`);
30
+ }
31
+ const matches = globSync('packages/*/*/package.json', { cwd: root })
32
+ .map(file => ({ file: join(root, file), manifest: manifestAt(join(root, file)) }))
33
+ .filter(entry => entry.manifest.name === name);
34
+ const selected = matches[0];
35
+ if (matches.length !== 1 || selected === undefined) {
36
+ throw new Error(`Official bundle ${name}: expected one workspace package in ${root}, found ${matches.length}; restore the checkout and run pnpm install`);
37
+ }
38
+ const { file, manifest } = selected;
39
+ const directory = realpathSync(dirname(file));
40
+ if (manifest.version !== version || manifest.private === true || manifest.dsh?.bundle === undefined) {
41
+ throw new Error(`Official bundle ${name}: ${file} must declare a public bundle at DSH version ${version}`);
42
+ }
43
+ const main = typeof manifest.main === 'string' ? resolve(directory, manifest.main) : undefined;
44
+ if (main === undefined || !existsSync(main) || !statSync(main).isFile()) {
45
+ throw new Error(`Official bundle ${name}: built runtime is missing in ${directory}; run pnpm install and pnpm run build in ${root}`);
46
+ }
47
+ for (const patch of bundlePatchPaths(directory, manifest.dsh.bundle)) {
48
+ if (!existsSync(patch) || !statSync(patch).isFile()) {
49
+ throw new Error(`Official bundle ${name}: bundle patch ${patch} is missing; restore the checkout`);
50
+ }
51
+ }
52
+ return { spec: `link:${directory}`, version };
53
+ }
54
+ //# sourceMappingURL=official-install-target.js.map
@@ -14,7 +14,7 @@ export function apply(ctx) {
14
14
  description: 'List plugins or bundles in the current profile, enable or disable them, install a bundle, or remove an installed bundle. Every action requires danger-full-access permission or approval for this call. Approval does not change the session permission mode. Changes affect every session in this profile. List first to obtain exact identifiers. Package installation can execute allowed build scripts. Live profiles apply changes immediately; startup profiles require restart. Incompatible DSH peer dependencies block installation and activation. Version exemptions risk crashes and data loss: warn the user and obtain explicit permission for the exact plugin and runtime versions before granting one.',
15
15
  parameters: {
16
16
  action: { type: 'string', required: true, enum: ['list_plugins', 'list_bundles', 'set_plugin', 'set_bundle', 'install_bundle', 'remove_bundle', 'list_version_exemptions', 'set_version_exemption'], description: 'Management operation.' },
17
- target: { type: 'string', description: 'Plugin entry id, bundle package name, or installation spec, according to action.' },
17
+ target: { type: 'string', description: 'Plugin entry id, bundle package name, or installation spec, according to action. For an Official catalog entry, pass list_bundles installTarget.spec to install_bundle.' },
18
18
  enabled: { type: 'boolean', description: 'Required for set operations; defaults to true for installation. For set_version_exemption, true grants and false revokes.' },
19
19
  runtimeVersion: { type: 'string', description: 'For set_version_exemption: exact DSH version from list_version_exemptions. Target must be the manifest package-name@version, not an alias or version range.' },
20
20
  acceptRisk: { type: 'boolean', description: 'For granting an exemption: true only after warning the user about possible crashes and data loss and receiving explicit permission for this exact plugin/runtime pair. General installation permission is not enough.' },
@@ -40,9 +40,18 @@ export interface BundleRowInfo {
40
40
  /** The Loader entry carrying this row, when exactly one live entry has its id. */
41
41
  entryId?: PluginEntryId;
42
42
  }
43
- /** One installed or installation-provided bundle. */
43
+ /** One shipped, profile-installed, or discoverable on-demand bundle. */
44
44
  export interface BundleInfo {
45
45
  name: string;
46
+ /** Project-maintained shipped optional bundle or on-demand catalog entry, independent of installation and selection. */
47
+ official: boolean;
48
+ /** Readable package location; on-demand catalog entries exclude undeclared transitive copies, and declared files may be missing. */
49
+ availability: 'installation' | 'profile' | 'missing';
50
+ /** Exact released Host version or same-checkout development link offered for an on-demand catalog entry. */
51
+ installTarget?: {
52
+ spec: string;
53
+ version: string;
54
+ };
46
55
  version?: string;
47
56
  /** Local display text with available translations or literal fallbacks, or a metadata diagnostic. */
48
57
  meta?: PluginLocalizedMeta;
@@ -50,8 +59,13 @@ export interface BundleInfo {
50
59
  description?: string;
51
60
  /** Selected in the profile manifest; a load error means its layer was skipped. */
52
61
  enabled: boolean;
53
- /** Whether the profile's own dependencies hold the package; false for a bundle the dsh installation supplies. */
62
+ /** Whether the profile declares this dependency; availability separately reports whether its files can be read. */
54
63
  installed: boolean;
64
+ /**
65
+ * Present for a profile dependency the installation does not also supply: the spec `pnpm add` accepts, with local
66
+ * paths made absolute and the user information of an http(s) URL removed.
67
+ */
68
+ source?: string;
55
69
  /**
56
70
  * Whether the installation ships the bundle for the person to switch on: named by the launcher's `OPTIONAL_BUNDLES`,
57
71
  * held by the installation's dependencies, selected by no shipped template, and never removable.
@@ -104,6 +118,8 @@ export interface ChangeResult {
104
118
  packageResult?: PackageResult;
105
119
  /** The bundle an installation added, once pnpm and the bundle check accepted it. */
106
120
  bundle?: string;
121
+ /** The installed bundle's manifest version, when declared; pnpm's `minimumReleaseAge` can make it older than the newest release. */
122
+ version?: string;
107
123
  /** Exact package names awaiting explicit script approval in the profile's pnpm settings, read after a failed run. */
108
124
  pendingBuilds?: string[];
109
125
  /** Package script permissions saved before this installation attempt. */
@@ -121,6 +137,8 @@ export type PluginInstallRequestId = Branded<'PluginInstallRequestId'>;
121
137
  /** Bundle installation defaults to activation; callers that offer cancellation supply their request id. */
122
138
  export interface InstallBundleOptions {
123
139
  enabled?: boolean;
140
+ /** Save the resolved dependency version without a range; applies to any bundle installation. */
141
+ saveExact?: boolean;
124
142
  requestId?: PluginInstallRequestId;
125
143
  /** Explicitly allow these pending packages' scripts for this profile, then install; a name no longer pending refuses the call. */
126
144
  approvedBuilds?: string[];
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-plugin-manager",
3
3
  "description": "Current-profile plugin and bundle management shared by dsh CLI, Web and agent tools",
4
- "version": "0.2.0-rc.2",
4
+ "version": "0.2.1-alpha.2",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -56,26 +56,26 @@
56
56
  ],
57
57
  "license": "MIT",
58
58
  "dependencies": {
59
- "execa": "^10.0.0",
59
+ "@deepseek-ai/dsh-app-boot": "0.2.1-alpha.2",
60
+ "@deepseek-ai/dsh-atomic-write": "0.2.1-alpha.2",
61
+ "@deepseek-ai/schemastery": "~3.18.5-alpha.1",
62
+ "execa": "^10.0.1",
60
63
  "yaml": "^2.9.0",
61
64
  "zod": "^4.4.3",
62
- "@deepseek-ai/dsh-app-boot": "0.2.0-rc.2",
63
- "@deepseek-ai/dsh-atomic-write": "0.2.0-rc.2",
64
- "@deepseek-ai/schemastery": "~3.18.4",
65
- "@deepseek-ai/dsh-util-values": "0.2.0-rc.2"
65
+ "@deepseek-ai/dsh-util-values": "0.2.1-alpha.2"
66
66
  },
67
67
  "peerDependencies": {
68
- "@deepseek-ai/cordis": "~4.0.4",
69
- "@deepseek-ai/cordis-plugin-loader": "~1.0.5",
70
- "@deepseek-ai/dsh-host-plugin-inventory": "0.2.0-rc.2",
71
- "@deepseek-ai/dsh-subprocess": "0.2.0-rc.2",
72
- "@deepseek-ai/dsh-typert-protocol": "0.2.0-rc.2",
73
- "@deepseek-ai/dsh-tools": "0.2.0-rc.2",
74
- "@deepseek-ai/dsh-brand": "0.2.0-rc.2",
75
- "@deepseek-ai/dsh-hmr": "0.2.0-rc.2",
76
- "@deepseek-ai/dsh-sandbox": "0.2.0-rc.2",
77
- "@deepseek-ai/dsh-sandbox-policy": "0.2.0-rc.2",
78
- "@deepseek-ai/dsh-user-approval": "0.2.0-rc.2"
68
+ "@deepseek-ai/cordis": "~4.0.5-alpha.1",
69
+ "@deepseek-ai/cordis-plugin-loader": "~1.0.6-alpha.1",
70
+ "@deepseek-ai/dsh-host-plugin-inventory": "0.2.1-alpha.2",
71
+ "@deepseek-ai/dsh-subprocess": "0.2.1-alpha.2",
72
+ "@deepseek-ai/dsh-typert-protocol": "0.2.1-alpha.2",
73
+ "@deepseek-ai/dsh-tools": "0.2.1-alpha.2",
74
+ "@deepseek-ai/dsh-hmr": "0.2.1-alpha.2",
75
+ "@deepseek-ai/dsh-brand": "0.2.1-alpha.2",
76
+ "@deepseek-ai/dsh-sandbox-policy": "0.2.1-alpha.2",
77
+ "@deepseek-ai/dsh-sandbox": "0.2.1-alpha.2",
78
+ "@deepseek-ai/dsh-user-approval": "0.2.1-alpha.2"
79
79
  },
80
80
  "peerDependenciesMeta": {
81
81
  "@deepseek-ai/dsh-hmr": {
@@ -86,22 +86,23 @@
86
86
  }
87
87
  },
88
88
  "devDependencies": {
89
- "@deepseek-ai/dsh-package-manifest": "0.2.0-rc.2",
90
- "@deepseek-ai/cordis-plugin-loader": "~1.0.5",
91
- "@deepseek-ai/dsh-subprocess": "0.2.0-rc.2",
92
- "@deepseek-ai/dsh-typert-protocol": "0.2.0-rc.2",
93
- "@deepseek-ai/dsh-tools": "0.2.0-rc.2",
94
- "@deepseek-ai/dsh-system-prompt": "0.2.0-rc.2",
95
- "@deepseek-ai/dsh-hmr": "0.2.0-rc.2",
96
- "@deepseek-ai/cordis-plugin-timer": "~1.1.6",
97
- "@deepseek-ai/cordis": "~4.0.4",
98
- "@deepseek-ai/dsh-brand": "0.2.0-rc.2",
99
- "@deepseek-ai/dsh-sandbox-policy": "0.2.0-rc.2",
100
- "@deepseek-ai/dsh-session": "0.2.0-rc.2",
101
- "@deepseek-ai/dsh-session-projection": "0.2.0-rc.2",
102
- "@deepseek-ai/dsh-sandbox": "0.2.0-rc.2",
103
- "@deepseek-ai/dsh-user-approval": "0.2.0-rc.2",
104
- "@deepseek-ai/dsh-agent": "0.2.0-rc.2",
105
- "@deepseek-ai/dsh-host-plugin-inventory": "0.2.0-rc.2"
89
+ "chokidar": "^4.0.3",
90
+ "@deepseek-ai/dsh-package-manifest": "0.2.1-alpha.2",
91
+ "@deepseek-ai/cordis": "~4.0.5-alpha.1",
92
+ "@deepseek-ai/cordis-plugin-loader": "~1.0.6-alpha.1",
93
+ "@deepseek-ai/dsh-host-plugin-inventory": "0.2.1-alpha.2",
94
+ "@deepseek-ai/dsh-subprocess": "0.2.1-alpha.2",
95
+ "@deepseek-ai/dsh-typert-protocol": "0.2.1-alpha.2",
96
+ "@deepseek-ai/dsh-tools": "0.2.1-alpha.2",
97
+ "@deepseek-ai/dsh-system-prompt": "0.2.1-alpha.2",
98
+ "@deepseek-ai/dsh-hmr": "0.2.1-alpha.2",
99
+ "@deepseek-ai/cordis-plugin-timer": "~1.1.7-alpha.1",
100
+ "@deepseek-ai/dsh-brand": "0.2.1-alpha.2",
101
+ "@deepseek-ai/dsh-sandbox-policy": "0.2.1-alpha.2",
102
+ "@deepseek-ai/dsh-agent": "0.2.1-alpha.2",
103
+ "@deepseek-ai/dsh-session": "0.2.1-alpha.2",
104
+ "@deepseek-ai/dsh-session-projection": "0.2.1-alpha.2",
105
+ "@deepseek-ai/dsh-sandbox": "0.2.1-alpha.2",
106
+ "@deepseek-ai/dsh-user-approval": "0.2.1-alpha.2"
106
107
  }
107
108
  }