@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.
package/lib/index.js CHANGED
@@ -1,16 +1,17 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { existsSync, readFileSync } from "node:fs";
2
+ import { existsSync, globSync, readFileSync, realpathSync, statSync } from "node:fs";
3
3
  import { mkdir, mkdtemp, open, readFile, rm } from "node:fs/promises";
4
- import { isAbsolute, join, resolve } from "node:path";
4
+ import { dirname, isAbsolute, join, resolve } from "node:path";
5
5
  import { pathToFileURL } from "node:url";
6
6
  import { withFileLock, writeFileAtomic } from "@deepseek-ai/dsh-atomic-write";
7
7
  import z from "@deepseek-ai/schemastery";
8
8
  import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
9
9
  import { pluginEntryId, readPluginInventory } from "@deepseek-ai/dsh-host-plugin-inventory";
10
- import { OPTIONAL_BUNDLES, PROFILE_COMPATIBILITY_FILENAME, bundlePatchPaths, composeEntries, evaluatePluginCompatibility, loadOptionalPatches, loadOverlayPatches, pluginCompatibilityWarning, readPluginMeta, readProfileCompatibility, readProfileManifest, readProfilePatches, readProfileVersionExemptions, reconcileProfilePatches, resolveBundleDir, resolveProfileDir, setProfileVersionExemption } from "@deepseek-ai/dsh-app-boot";
10
+ import { OFFICIAL_ON_DEMAND_CATALOG, OPTIONAL_BUNDLES, PROFILE_COMPATIBILITY_FILENAME, bundlePatchPaths, composeEntries, evaluatePluginCompatibility, getDshRuntimeVersion, loadOptionalPatches, loadOverlayPatches, pluginCompatibilityWarning, readPluginMeta, readProfileCompatibility, readProfileManifest, readProfilePatches, readProfileVersionExemptions, reconcileProfilePatches, resolveBundleDir, resolveProfileDir, setProfileVersionExemption } from "@deepseek-ai/dsh-app-boot";
11
11
  import { once } from "node:events";
12
12
  import { execa } from "execa";
13
13
  import { scrubbedParentEnv } from "@deepseek-ai/dsh-subprocess";
14
+ import { homedir } from "node:os";
14
15
  import { isAlias, isMap, isNode, isScalar, isSeq, parseDocument, visit } from "yaml";
15
16
  //#region lib/types/install-spec.js
16
17
  /**
@@ -115,6 +116,32 @@ function parseInstallSpec(raw) {
115
116
  range
116
117
  };
117
118
  }
119
+ /** A recorded dependency value pnpm resolves outside a registry: a protocol other than a registry alias, or an scp-like git address. */
120
+ const NON_REGISTRY_VALUE = /^(?!(?:npm|jsr|workspace|catalog):)[a-z][a-z0-9+.-]*:|^[^@/:\s]+@[^:/\s]+:/i;
121
+ /** The user information of an http(s) URL, up to the last `@` before the path, which holds a password or access token when present. */
122
+ const HTTP_USER_INFO = /^((?:git\+)?https?:\/\/)[^/]*@/i;
123
+ /**
124
+ * The spec `pnpm add` accepts for a dependency the profile manifest records. A git address, URL, or other
125
+ * non-registry protocol is the recorded value without the user information of an http(s) URL, whose query string
126
+ * is kept; a `file:` or `link:` path, which pnpm records relative to the profile or with `~` for the home
127
+ * directory, becomes absolute. A registry range, tag, or alias follows the dependency name after `@`, as does a
128
+ * non-registry value installed under a name other than the package's own.
129
+ * @param name - the dependency name.
130
+ * @param recorded - the value the profile manifest records for it.
131
+ * @param profileDir - the profile directory a relative local path is resolved against.
132
+ * @param packageName - the name the installed package's manifest declares, when it could be read.
133
+ * @returns the installable spec.
134
+ */
135
+ function dependencySpec(name, recorded, profileDir, packageName = name) {
136
+ const local = /^(file|link):(.*)$/s.exec(recorded);
137
+ let spec;
138
+ if (local !== null) {
139
+ const path = local[2].replace(/^~(?=$|[\\/])/, () => homedir());
140
+ spec = `${local[1]}:${resolve(profileDir, path)}`;
141
+ } else if (NON_REGISTRY_VALUE.test(recorded)) spec = recorded.replace(HTTP_USER_INFO, "$1");
142
+ else return `${name}@${recorded}`;
143
+ return packageName === name ? spec : `${name}@${spec}`;
144
+ }
118
145
  //#endregion
119
146
  //#region lib/types/run-tree.js
120
147
  /** Waiting for one package run's process tree to disappear before its caller touches the profile. */
@@ -792,6 +819,48 @@ function classifyInstallFailure(facts) {
792
819
  for (const [kind, pattern] of LOG_KINDS) if (pattern.test(facts.log)) return kind;
793
820
  return "unknown";
794
821
  }
822
+ //#endregion
823
+ //#region lib/types/official-install-target.js
824
+ /** Select Official bundle packages from the running installation without importing provider code. */
825
+ function manifestAt(file) {
826
+ return JSON.parse(readFileSync(file, "utf8"));
827
+ }
828
+ /**
829
+ * Offer a source checkout link or an exact published package version. Source installations are
830
+ * identified by the real CLI manifest and its unpacked workspace dependencies, including Desktop's
831
+ * development symlink. An incomplete checkout is an error, never a registry fallback.
832
+ * @param name - Official catalog package name.
833
+ * @param installAnchor - Running application's DSH package.json, possibly reached through a symlink.
834
+ * @param version - Running DSH version used by its release family.
835
+ * @returns An ordinary package-manager spec and its package version.
836
+ * @throws when a source installation lacks its workspace package, bundle patch, or built runtime.
837
+ */
838
+ function officialBundleInstallTarget(name, installAnchor, version) {
839
+ const anchor = realpathSync(installAnchor);
840
+ const installation = manifestAt(anchor);
841
+ if (!(installation.name === "@deepseek-ai/dsh" && Object.values(installation.dependencies ?? {}).some((value) => value.startsWith("workspace:")))) return {
842
+ spec: `${name}@${version}`,
843
+ version
844
+ };
845
+ const root = resolve(dirname(anchor), "../..");
846
+ if (anchor !== join(root, "apps", "cli", "package.json") || !existsSync(join(root, "package.json")) || manifestAt(join(root, "package.json")).name !== "@deepseek-ai/dsh-root") throw new Error(`Official bundle ${name}: source DSH installation ${anchor} requires its complete workspace checkout`);
847
+ const matches = globSync("packages/*/*/package.json", { cwd: root }).map((file) => ({
848
+ file: join(root, file),
849
+ manifest: manifestAt(join(root, file))
850
+ })).filter((entry) => entry.manifest.name === name);
851
+ const selected = matches[0];
852
+ if (matches.length !== 1 || selected === void 0) throw new Error(`Official bundle ${name}: expected one workspace package in ${root}, found ${matches.length}; restore the checkout and run pnpm install`);
853
+ const { file, manifest } = selected;
854
+ const directory = realpathSync(dirname(file));
855
+ if (manifest.version !== version || manifest.private === true || manifest.dsh?.bundle === void 0) throw new Error(`Official bundle ${name}: ${file} must declare a public bundle at DSH version ${version}`);
856
+ const main = typeof manifest.main === "string" ? resolve(directory, manifest.main) : void 0;
857
+ if (main === void 0 || !existsSync(main) || !statSync(main).isFile()) throw new Error(`Official bundle ${name}: built runtime is missing in ${directory}; run pnpm install and pnpm run build in ${root}`);
858
+ for (const patch of bundlePatchPaths(directory, manifest.dsh.bundle)) if (!existsSync(patch) || !statSync(patch).isFile()) throw new Error(`Official bundle ${name}: bundle patch ${patch} is missing; restore the checkout`);
859
+ return {
860
+ spec: `link:${directory}`,
861
+ version
862
+ };
863
+ }
795
864
  /** Public npmmirror URL shared by the fallback configuration and public-registry comparison. */
796
865
  const NPMMIRROR_REGISTRY = "https://registry.npmmirror.com/";
797
866
  /**
@@ -913,6 +982,10 @@ async function writePluginEnabled(filename, id, name, enabled) {
913
982
  //#endregion
914
983
  //#region lib/types/build-approval.js
915
984
  /** Approve pnpm's pending dependency scripts in the current profile's workspace settings. */
985
+ /** Value pnpm leaves for a build script that still needs a decision. */
986
+ const UNDECIDED_BUILD = "set this to true or false";
987
+ /** Where a non-interactive run names the build scripts it skipped. */
988
+ const IGNORED_BUILDS = /\bIgnored build scripts: (.+)/;
916
989
  async function readPolicy(dir) {
917
990
  let text;
918
991
  try {
@@ -929,17 +1002,41 @@ async function readPolicy(dir) {
929
1002
  visit(builds ?? null, (_key, node) => {
930
1003
  if (isAlias(node) || isNode(node) && "anchor" in node && node.anchor) throw new Error("allowBuilds must not contain YAML anchors or aliases");
931
1004
  });
1005
+ const keys = isMap(builds) ? builds.items.flatMap(({ key }) => isScalar(key) && typeof key.value === "string" ? [key.value] : []) : [];
932
1006
  return {
933
1007
  document,
934
- pending: isMap(builds) ? builds.items.flatMap(({ key, value }) => isScalar(key) && typeof key.value === "string" && !/[*?]/.test(key.value) && isScalar(value) && value.value === "set this to true or false" ? [key.value] : []) : []
1008
+ pending: isMap(builds) ? builds.items.flatMap(({ key, value }) => isScalar(key) && typeof key.value === "string" && !/[*?]/.test(key.value) && isScalar(value) && value.value === UNDECIDED_BUILD ? [key.value] : []) : [],
1009
+ keys
935
1010
  };
936
1011
  }
937
- /** Read package names left undecided by pnpm 11, including after installation cleanup.
1012
+ /**
1013
+ * Record the build scripts a failed run reported as ignored, then list every name awaiting a decision.
1014
+ *
1015
+ * pnpm writes those policy entries itself only when it can prompt, and the
1016
+ * manager always runs it non-interactively, so the run's own report is the
1017
+ * record that survives.
938
1018
  * @param dir Current profile directory.
1019
+ * @param output Captured output of the failed run.
939
1020
  * @returns Exact package names awaiting a build decision; wildcard rules are excluded.
940
1021
  */
941
- async function readPendingBuilds(dir) {
942
- return (await readPolicy(dir)).pending;
1022
+ async function recordPendingBuilds(dir, output) {
1023
+ const { document, pending, keys } = await readPolicy(dir);
1024
+ const known = new Set(keys);
1025
+ const added = ignoredBuildNames(output).filter((name) => !known.has(name));
1026
+ if (added.length === 0) return pending;
1027
+ for (const name of added) document.setIn(["allowBuilds", name], UNDECIDED_BUILD);
1028
+ await writeFileAtomic(join(dir, "pnpm-workspace.yaml"), String(document), { mode: 384 });
1029
+ return [...pending, ...added];
1030
+ }
1031
+ /**
1032
+ * Read the package names a pnpm run reported as ignored build scripts.
1033
+ * @param output Captured output of the run.
1034
+ * @returns Exact names in report order; wildcard patterns are excluded.
1035
+ */
1036
+ function ignoredBuildNames(output) {
1037
+ const reported = IGNORED_BUILDS.exec(output)?.[1];
1038
+ if (reported === void 0) return [];
1039
+ return reported.split(",").map((name) => name.trim()).filter((name) => name !== "" && !/[*?]/.test(name));
943
1040
  }
944
1041
  /** Persist approval without running scripts; the caller holds the profile manifest lock.
945
1042
  * @param dir Current profile directory.
@@ -1448,70 +1545,124 @@ let PluginManager = (() => {
1448
1545
  };
1449
1546
  });
1450
1547
  }
1451
- /** Read the profile's installed bundles, the bundles this dsh installation supplies, and the selected names that are not bundles.
1548
+ /** Read installed, installation-provided, and offline Official catalog bundles, plus selected non-bundle names.
1452
1549
  * A dependency without a bundle patch is listed, as a `not-bundle` problem, only while it is selected.
1453
- * @returns Package versions, manifest descriptions, rows, optional display metadata, activation selections,
1454
- * whether the installation offers the bundle, and removal availability.
1550
+ * @returns Package versions, manifest descriptions, the installable spec of profile dependencies, rows, optional
1551
+ * display metadata, activation selections, whether the installation offers the bundle, and removal availability.
1455
1552
  */
1456
1553
  listBundles() {
1457
1554
  const manifest = readProfileManifest("dsh", this.profile.dir);
1458
1555
  const exemptions = readProfileVersionExemptions(this.profile.dir);
1459
1556
  const selected = manifest.dsh?.profile?.bundles ?? [];
1460
- const dependencies = Object.keys(manifest.dependencies ?? {});
1557
+ const recorded = manifest.dependencies ?? {};
1558
+ const dependencies = Object.keys(recorded);
1461
1559
  const installation = JSON.parse(readFileSync(this.profile.installAnchor, "utf8"));
1560
+ const catalog = new Map(OFFICIAL_ON_DEMAND_CATALOG.map((entry) => [entry.packageName, entry]));
1462
1561
  const names = [...new Set([
1463
1562
  ...selected,
1464
1563
  ...dependencies,
1465
- ...Object.keys(installation.dependencies ?? {})
1564
+ ...Object.keys(installation.dependencies ?? {}),
1565
+ ...catalog.keys()
1466
1566
  ])];
1467
1567
  const bundles = [];
1568
+ const runtimeVersion = getDshRuntimeVersion();
1468
1569
  for (const name of names) {
1469
1570
  const installed = dependencies.includes(name);
1470
1571
  const optional = OPTIONAL_BUNDLES.includes(name);
1471
- const removable = installed && !Object.hasOwn(installation.dependencies ?? {}, name);
1472
1572
  const enabled = selected.includes(name);
1573
+ const shipped = Object.hasOwn(installation.dependencies ?? {}, name);
1574
+ const offered = catalog.get(name);
1575
+ const official = optional || offered !== void 0;
1576
+ let target = {};
1577
+ let targetError;
1578
+ if (offered !== void 0) try {
1579
+ target = { installTarget: officialBundleInstallTarget(name, this.profile.installAnchor, runtimeVersion) };
1580
+ } catch (error) {
1581
+ targetError = managementError(error);
1582
+ }
1583
+ const catalogMeta = offered === void 0 ? {} : { meta: offered.meta };
1584
+ if (offered !== void 0 && !installed && !shipped) {
1585
+ bundles.push({
1586
+ name,
1587
+ official,
1588
+ availability: "missing",
1589
+ ...catalogMeta,
1590
+ ...target,
1591
+ enabled,
1592
+ installed,
1593
+ optional,
1594
+ removable: enabled,
1595
+ rows: [],
1596
+ overrides: [],
1597
+ ...targetError !== void 0 ? { error: targetError } : enabled ? { error: { code: "not-bundle" } } : {}
1598
+ });
1599
+ continue;
1600
+ }
1601
+ const owned = installed && !shipped;
1602
+ const removable = owned || enabled && !installed && !shipped;
1603
+ const sourceOf = (packageName) => owned ? { source: dependencySpec(name, recorded[name], this.profile.dir, packageName) } : {};
1473
1604
  const readOnlyReason = this.protectsManager(name) ? "management-required" : void 0;
1605
+ let availability = "missing";
1606
+ let version;
1474
1607
  try {
1475
1608
  const info = bundleManifest(name, this.profile.dir, this.profile.installAnchor);
1609
+ availability = shipped ? "installation" : "profile";
1476
1610
  if (info === void 0) {
1477
- if (enabled) bundles.push({
1611
+ if (enabled || offered !== void 0) bundles.push({
1478
1612
  name,
1613
+ official,
1614
+ availability,
1615
+ ...catalogMeta,
1616
+ ...target,
1617
+ ...sourceOf(),
1479
1618
  enabled,
1480
1619
  installed,
1481
1620
  optional,
1482
1621
  removable: removable && readOnlyReason === void 0,
1483
1622
  ...readOnlyReason === void 0 ? {} : { readOnlyReason },
1484
- error: { code: "not-bundle" },
1623
+ error: targetError ?? { code: "not-bundle" },
1485
1624
  rows: [],
1486
1625
  overrides: []
1487
1626
  });
1488
1627
  continue;
1489
1628
  }
1629
+ version = info.version;
1490
1630
  const compatibility = evaluatePluginCompatibility(info, exemptions);
1491
1631
  if (compatibility !== void 0 && !compatibility.exempted) throw new ManagementFailure("incompatible-version", [incompatiblePlugin(compatibility)]);
1492
1632
  const dir = resolveBundleDir("dsh", name, this.profile.installAnchor, this.profile.dir);
1493
- const meta = readPluginMeta(info.name ?? name, pathToFileURL(join(dir, "package.json")).href);
1633
+ const meta = offered?.meta ?? readPluginMeta(info.name ?? name, pathToFileURL(join(dir, "package.json")).href);
1494
1634
  bundles.push({
1495
1635
  name,
1496
- ...info.version === void 0 ? {} : { version: info.version },
1636
+ official,
1637
+ availability,
1638
+ ...target,
1639
+ ...version === void 0 ? {} : { version },
1497
1640
  ...info.description === void 0 || info.description === "" ? {} : { description: info.description },
1498
1641
  ...meta === void 0 ? {} : { meta },
1642
+ ...sourceOf(info.name),
1499
1643
  enabled,
1500
1644
  installed,
1501
1645
  optional,
1502
1646
  removable: removable && readOnlyReason === void 0,
1503
1647
  ...readOnlyReason === void 0 ? {} : { readOnlyReason },
1504
- ...this.declaredRows(name, info)
1648
+ ...this.declaredRows(name, info),
1649
+ ...targetError === void 0 ? {} : { error: targetError }
1505
1650
  });
1506
1651
  } catch (error) {
1507
- if (enabled || installed) bundles.push({
1652
+ if (enabled || installed || optional || offered !== void 0) bundles.push({
1508
1653
  name,
1654
+ official,
1655
+ availability,
1656
+ ...catalogMeta,
1657
+ ...target,
1658
+ ...version === void 0 ? {} : { version },
1659
+ ...sourceOf(),
1509
1660
  enabled,
1510
1661
  installed,
1511
1662
  optional,
1512
1663
  removable: removable && readOnlyReason === void 0,
1513
1664
  ...readOnlyReason === void 0 ? {} : { readOnlyReason },
1514
- error: managementError(error),
1665
+ error: targetError ?? managementError(error),
1515
1666
  rows: [],
1516
1667
  overrides: []
1517
1668
  });
@@ -1670,7 +1821,9 @@ let PluginManager = (() => {
1670
1821
  setBundleEnabled(name, enabled) {
1671
1822
  return this.change((result) => this.configure(async () => {
1672
1823
  await this.selectBundle(name, enabled);
1824
+ if (enabled) await this.refreshPackages();
1673
1825
  result.warnings = await this.reload(enabled ? this.bundleRows(name).map((row) => row.id) : []);
1826
+ if (!enabled && this.ownerContext.get("hmr") !== void 0) await this.refreshPackages();
1674
1827
  }), {
1675
1828
  stage: "enable",
1676
1829
  target: name,
@@ -1685,7 +1838,7 @@ let PluginManager = (() => {
1685
1838
  * `package.json` and `pnpm-lock.yaml` as they were; downloaded files can stay.
1686
1839
  * @param spec One package spec, including local paths relative to the invocation directory.
1687
1840
  * @param options Whether to activate the installed bundle (defaults to true), the request id a cancellation names,
1688
- * the pending build scripts to allow for this profile before pnpm runs, and the registry asked first.
1841
+ * the pending build scripts to allow, whether to save an exact dependency, and the registry asked first.
1689
1842
  * @returns Package-manager diagnostics, the registries asked, and the observed activation outcome.
1690
1843
  */
1691
1844
  installBundle(spec, options) {
@@ -1714,6 +1867,7 @@ let PluginManager = (() => {
1714
1867
  const files = await this.readRestoredFiles();
1715
1868
  const before = readProfileManifest("dsh", this.profile.dir).dependencies ?? {};
1716
1869
  let name;
1870
+ let version;
1717
1871
  try {
1718
1872
  result.registries = [];
1719
1873
  const connection = checkGithubConnection(parsedForRegistry(spec), this.profile.dir, {
@@ -1749,6 +1903,7 @@ let PluginManager = (() => {
1749
1903
  run = await this.runPnpm([
1750
1904
  "add",
1751
1905
  spec,
1906
+ ...options?.saveExact === true ? ["--save-exact"] : [],
1752
1907
  ...registryArguments(registry)
1753
1908
  ], control.abort.signal, requestId);
1754
1909
  result.packageResult = run;
@@ -1769,7 +1924,7 @@ let PluginManager = (() => {
1769
1924
  if (succeeded) delete result.failedAt;
1770
1925
  if (!succeeded) {
1771
1926
  try {
1772
- result.pendingBuilds = await readPendingBuilds(this.profile.dir);
1927
+ result.pendingBuilds = await recordPendingBuilds(this.profile.dir, run.output);
1773
1928
  } catch (error) {
1774
1929
  this.ownerContext.logger.warn("Could not read pending build approvals after pnpm failed", error);
1775
1930
  }
@@ -1777,7 +1932,11 @@ let PluginManager = (() => {
1777
1932
  }
1778
1933
  const after = readProfileManifest("dsh", this.profile.dir).dependencies ?? {};
1779
1934
  const installed = Object.keys(after).filter((name) => before[name] !== after[name]);
1780
- if (installed.length === 0) installed.push(...Object.keys(after).filter((name) => spec === name || spec.startsWith(`${name}@`)));
1935
+ if (installed.length === 0) {
1936
+ const parsed = parsedForRegistry(spec);
1937
+ const localName = parsed.kind === "path" ? readProfileManifest("dsh", parsed.path).name : void 0;
1938
+ installed.push(...Object.keys(after).filter((name) => name === localName || spec === name || spec.startsWith(`${name}@`)));
1939
+ }
1781
1940
  const target = installed[0];
1782
1941
  if (installed.length !== 1 || target === void 0) throw new ManagementFailure("ambiguous-install");
1783
1942
  name = target;
@@ -1787,6 +1946,7 @@ let PluginManager = (() => {
1787
1946
  const compatibility = evaluatePluginCompatibility(manifest, readProfileVersionExemptions(this.profile.dir));
1788
1947
  if (compatibility !== void 0 && !compatibility.exempted) throw new ManagementFailure("incompatible-version", [incompatiblePlugin(compatibility)]);
1789
1948
  for (const file of bundlePatchPaths(dir, manifest.dsh.bundle)) loadOverlayPatches("dsh", file);
1949
+ version = manifest.version;
1790
1950
  } catch (error) {
1791
1951
  await this.restoreFiles(files);
1792
1952
  throw error;
@@ -1794,11 +1954,13 @@ let PluginManager = (() => {
1794
1954
  control.phase = "applying";
1795
1955
  announce("applying");
1796
1956
  result.bundle = name;
1957
+ if (version !== void 0) result.version = version;
1797
1958
  result.target = name;
1798
1959
  result.stage = "enable";
1799
1960
  return this.configure(async () => {
1800
1961
  if (options?.enabled !== false) await this.selectBundle(name, true);
1801
1962
  if (Object.hasOwn(before, name)) return "restart-required";
1963
+ await this.refreshPackages();
1802
1964
  if (options?.enabled !== false) result.warnings = await this.reload();
1803
1965
  });
1804
1966
  }, {
@@ -1837,13 +1999,14 @@ let PluginManager = (() => {
1837
1999
  await control.result.then(() => void 0, () => void 0);
1838
2000
  return { status: "cancelled" };
1839
2001
  }
1840
- /** Unload and remove a profile-owned bundle dependency through dsh plugin's pnpm path.
1841
- * @param name Installed dependency name.
2002
+ /** Unload and remove a profile-owned bundle dependency through dsh plugin's pnpm path; a selected name no
2003
+ * dependency holds is only deselected.
2004
+ * @param name Installed dependency or selected bundle name.
1842
2005
  * @returns Removal diagnostics and the remaining profile state.
1843
2006
  */
1844
2007
  removeBundle(name) {
1845
2008
  return this.change(async (result) => {
1846
- await this.configure(async () => {
2009
+ if (!await this.configure(async () => {
1847
2010
  const bundle = (await this.listBundles()).find((item) => item.name === name);
1848
2011
  if (bundle === void 0 || !bundle.removable) throw new ManagementFailure("not-removable");
1849
2012
  if (this.ownerContext.get("hmr") === void 0 && (this.profile.startedBundles.includes(name) || bundle.error === void 0 && this.bundleRows(name).some((row) => [...this.ctx.loader.entries()].some((entry) => entry.options.id === row.id && entry.fiber !== void 0)))) throw new ManagementFailure("stop-profile");
@@ -1853,9 +2016,11 @@ let PluginManager = (() => {
1853
2016
  result.warnings = await this.reload();
1854
2017
  }
1855
2018
  if ([...this.ctx.loader.entries()].some((entry) => entry.fiber?.uid != null && contributions.some((row) => row.id === entry.options.id && row.name === entry.options.name))) throw new ManagementFailure("bundle-in-use");
1856
- });
2019
+ return bundle.installed;
2020
+ })) return;
1857
2021
  result.packageResult = await this.runPnpm(["remove", name]);
1858
2022
  if (result.packageResult.exitCode !== 0 || result.packageResult.timedOut === true) throw new Error(result.packageResult.output);
2023
+ await this.configure(() => this.refreshPackages());
1859
2024
  }, {
1860
2025
  stage: "remove",
1861
2026
  target: name
@@ -1881,7 +2046,7 @@ let PluginManager = (() => {
1881
2046
  const rows = [];
1882
2047
  const packages = this.ctx.get("pluginPackages");
1883
2048
  for (const row of flatten(composeEntries([patches.filter((item) => item.insert !== void 0)]))) {
1884
- if (typeof row.id !== "string" || typeof row.name !== "string") continue;
2049
+ if (typeof row.id !== "string" || typeof row.name !== "string" || row.group === true) continue;
1885
2050
  const active = live.get(row.id);
1886
2051
  const entryId = active?.entryId;
1887
2052
  const base = active?.baseUrl ?? pathToFileURL(join(dir, "package.json")).href;
@@ -2028,6 +2193,13 @@ let PluginManager = (() => {
2028
2193
  };
2029
2194
  return hmr === void 0 ? apply() : hmr.runExclusive(apply);
2030
2195
  }
2196
+ async refreshPackages() {
2197
+ if (this.ownerContext.get("hmr") === void 0) {
2198
+ const selected = readProfileManifest("dsh", this.profile.dir).dsh?.profile?.bundles ?? [];
2199
+ if (this.profile.startedBundles.some((name) => !selected.includes(name))) return;
2200
+ }
2201
+ await this.ownerContext.get("pluginPackages")?.refresh();
2202
+ }
2031
2203
  async reload(requiredIds = []) {
2032
2204
  if (this.ownerContext.get("hmr") === void 0) return [];
2033
2205
  return reconcileProfilePatches(this.ownerContext.root, readProfilePatches("dsh", this.profile), "dsh", requiredIds);