dsh-plugin-shop 0.9.0 → 0.9.1-beta.0

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
@@ -180,11 +180,20 @@ function httpOrigin(baseUrl, fetchImpl) {
180
180
  * Pure — bytes in, a path-to-bytes map out. It handles exactly what `npm
181
181
  * pack` emits and refuses everything else loudly, because the alternative to
182
182
  * a small strict reader is a fourth runtime dependency (design §4). */
183
- /** Bytes up to the first NUL, as ASCII. Tar pads its fixed-width text fields
184
- * with NULs, so a plain toString would carry them into the path. */
183
+ /** Bytes up to the first NUL, one character for each byte. Tar pads its
184
+ * fixed-width text fields with NULs, so a plain toString would carry them into
185
+ * the path.
186
+ *
187
+ * `latin1`, not `ascii`: a name is bytes, and `ascii` drops the high bit of
188
+ * every one, so a name written in UTF-8 came back as a different string (`é`,
189
+ * C3 A9, as `C)`) that could be another file's name. One character for each
190
+ * byte is lossless and is the same string for a name that is ASCII, which is
191
+ * every name the npm transport reads. A caller that wants the characters of a
192
+ * UTF-8 name decodes `Buffer.from(name, 'latin1')`, or looks a name up as
193
+ * `Buffer.from(wanted, 'utf8').toString('latin1')`. */
185
194
  function cstring(field) {
186
195
  const end = field.indexOf(0);
187
- return field.subarray(0, end === -1 ? field.length : end).toString("ascii");
196
+ return field.subarray(0, end === -1 ? field.length : end).toString("latin1");
188
197
  }
189
198
  /** Tar sizes are octal text, NUL- or space-terminated. An unparseable size
190
199
  * would desynchronise every subsequent header, so it throws rather than
@@ -206,13 +215,20 @@ function assertContained(path) {
206
215
  /**
207
216
  * Parse an uncompressed tar archive into path → bytes.
208
217
  *
209
- * Directory entries and every non-regular type (symlinks, pax and GNU
210
- * extension headers) are skipped: npm packs regular files under `package/`,
211
- * and a catalog tarball that needs anything else is not one we published.
218
+ * Directory entries and the type flags that carry no file are skipped: npm
219
+ * packs regular files under `package/`, and a catalog tarball that needs
220
+ * anything else is not one we published. A pax (`x`) or GNU (`L`) header is not
221
+ * a file either, but it NAMES the next one: a path too long or too wide for a
222
+ * ustar header arrives this way, and skipping the header without reading it
223
+ * left its file under a truncated name, reported missing from an archive that
224
+ * ships it ({@link pathFromPax}).
225
+ * Each path is spelled by its bytes, one character each ({@link cstring}).
212
226
  */
213
227
  function readTar(buffer) {
214
228
  const files = /* @__PURE__ */ new Map();
215
229
  let offset = 0;
230
+ /** The name a pax or GNU header gives the header after it. */
231
+ let extendedPath = null;
216
232
  while (offset + 512 <= buffer.length) {
217
233
  const header = buffer.subarray(offset, offset + 512);
218
234
  if (header.every((byte) => byte === 0)) break;
@@ -220,13 +236,49 @@ function readTar(buffer) {
220
236
  const prefix = cstring(header.subarray(345, 500));
221
237
  const size = parseOctal(header.subarray(124, 136));
222
238
  const typeflag = String.fromCharCode(header[156] ?? 0);
223
- const path = prefix === "" ? name : `${prefix}/${name}`;
224
- offset += 512;
225
- if (typeflag === "0" || typeflag === "\0") files.set(assertContained(path), buffer.subarray(offset, offset + size));
226
- offset += Math.ceil(size / 512) * 512;
239
+ const data = buffer.subarray(offset + 512, offset + 512 + size);
240
+ offset += 512 + Math.ceil(size / 512) * 512;
241
+ if (typeflag === "x") {
242
+ extendedPath = pathFromPax(data) ?? extendedPath;
243
+ continue;
244
+ }
245
+ if (typeflag === "L") {
246
+ extendedPath = cstring(data);
247
+ continue;
248
+ }
249
+ const path = extendedPath ?? (prefix === "" ? name : `${prefix}/${name}`);
250
+ extendedPath = null;
251
+ if (typeflag === "0" || typeflag === "\0") files.set(assertContained(path), data);
227
252
  }
228
253
  return files;
229
254
  }
255
+ /**
256
+ * The `path` record of a pax extended header, in the reader's spelling, or null
257
+ * where the header names none.
258
+ *
259
+ * A pax record is `<length> <key>=<value>\n`, where `<length>` counts the whole
260
+ * record in bytes, its own digits included. The value is UTF-8 text, and the
261
+ * reader spells a name one character for each byte.
262
+ */
263
+ function pathFromPax(data) {
264
+ let at = 0;
265
+ while (at < data.length) {
266
+ const space = data.indexOf(32, at);
267
+ if (space === -1) return null;
268
+ const length = Number.parseInt(data.subarray(at, space).toString("latin1"), 10);
269
+ if (!Number.isSafeInteger(length) || length <= 0 || at + length > data.length) return null;
270
+ const record = data.subarray(at, at + length);
271
+ const equals = record.indexOf(61, space - at);
272
+ if (equals !== -1) {
273
+ if (record.subarray(space - at + 1, equals).toString("utf8") === "path") {
274
+ const end = record[record.length - 1] === 10 ? record.length - 1 : record.length;
275
+ return record.subarray(equals + 1, end).toString("latin1");
276
+ }
277
+ }
278
+ at += length;
279
+ }
280
+ return null;
281
+ }
230
282
  //#endregion
231
283
  //#region src/host/npm-origin.ts
232
284
  /** The npm transport (design §2, §3): the catalog as a package.
@@ -520,6 +572,45 @@ function holderLabel(spec, name) {
520
572
  if (spec.trim() === "") return "\"\"";
521
573
  return spec.length > HOLDER_SPEC_CAP ? `${spec.slice(0, 79)}…` : spec;
522
574
  }
575
+ //#endregion
576
+ //#region src/shared/npm-name.ts
577
+ /**
578
+ * The npm package-name grammar a catalogue row's `name` must satisfy.
579
+ *
580
+ * ONE definition, read in three places: the Host's entry schema
581
+ * (`host/catalog.ts`), the registry's gate (`registry/scripts/src/gate.ts`)
582
+ * and `emit`'s last-line invariant. The dependency can only point this way —
583
+ * the package's tsconfig pins `rootDir: "src"`, so the registry reaches in
584
+ * and nothing here can reach out (the same reason `shop-like.ts` lives here).
585
+ *
586
+ * Why it is narrower than npm's own rule: `validate-npm-package-name` accepts
587
+ * a scope beginning with `_` or `.` — `@_iluxury/dsh-routerai-price-watcher`
588
+ * is valid for a NEW publication — while this grammar requires the scope's
589
+ * first character to be a letter or digit. That was a deliberate argv-safety
590
+ * choice when the rule lived only in the client: the name becomes half of an
591
+ * install spec, and everything this excludes is shell punctuation. It stopped
592
+ * being merely a client rule the moment a catalogue shipped, because the
593
+ * clients parse the WHOLE data file as one zod object: a single name outside
594
+ * this grammar costs every listing, not one. On 2026-10-30
595
+ * `@_iluxury/dsh-routerai-price-watcher` did exactly that — valid on npm, no
596
+ * gate rule to catch it, and every 0.8.0+ shop answered 「暂时无法读取目录」
597
+ * for a 21,493-row catalogue. The gate now admits only what this parses.
598
+ *
599
+ * The narrow side is therefore the WIRE CONTRACT, frozen by the installed
600
+ * base: widening it in a client release also widens the gate, because this is
601
+ * one constant, and the catalogue would then carry names every older client
602
+ * still refuses. Do not widen it without a way to know which client versions
603
+ * are installed; npm's per-version download counts cannot answer that (the
604
+ * measurement is in `emit.ts`'s `peers` comment).
605
+ *
606
+ * @module shared/npm-name
607
+ */
608
+ /**
609
+ * An optional `@scope/`, then a name starting with a letter or digit and
610
+ * continuing in lowercase letters, digits, `.`, `_` or `-`. No uppercase, no
611
+ * leading dot or underscore on either segment, and no shell punctuation.
612
+ */
613
+ const CATALOG_NPM_NAME = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
523
614
  /** A cached catalog younger than this is served without touching the network. */
524
615
  const FRESH_MS = 3e5;
525
616
  /** How long a probe may take before the race gives up on that origin. Long
@@ -550,9 +641,6 @@ async function withCommitBudget(id, what, start) {
550
641
  clearTimeout(timer);
551
642
  }
552
643
  }
553
- /** npm's package-name grammar. The value becomes half of an install spec, so
554
- * aliases and shell punctuation must not reach the CLI. */
555
- const NPM_NAME = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
556
644
  /** Canonical SemVer 2.0.0, including legal prerelease/build metadata. */
557
645
  const NPM_VERSION = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*)?(?:\+[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*)?$/;
558
646
  /** The repository binding used to build a GitHub install spec. */
@@ -653,10 +741,11 @@ const entrySchema = z.object({
653
741
  unpackedSize: z.number().int().nonnegative().optional(),
654
742
  installSize: z.number().int().nonnegative().optional(),
655
743
  archived: z.boolean().optional(),
656
- forked: z.boolean().optional()
744
+ forked: z.boolean().optional(),
745
+ icon: z.string().regex(/^[0-9a-f]{64}$/).optional()
657
746
  }).superRefine((entry, ctx) => {
658
747
  if (entry.source === "npm") {
659
- if (!NPM_NAME.test(entry.name)) ctx.addIssue({
748
+ if (!CATALOG_NPM_NAME.test(entry.name)) ctx.addIssue({
660
749
  code: "custom",
661
750
  path: ["name"],
662
751
  message: `npm entry name ${JSON.stringify(entry.name)} is outside npm package-name grammar`
@@ -734,6 +823,10 @@ const pointerSchema = z.object({
734
823
  stars: z.object({
735
824
  url: z.string(),
736
825
  sha256: z.string()
826
+ }).optional(),
827
+ icons: z.object({
828
+ url: z.string(),
829
+ sha256: z.string()
737
830
  }).optional()
738
831
  });
739
832
  const nodeFs$1 = {
@@ -757,6 +850,34 @@ function parseStarsText(text) {
757
850
  return {};
758
851
  }
759
852
  }
853
+ /** The longest a data URL holding MAX_CATALOG_ICON_BYTES can be: the base64
854
+ * alphabet is 4 characters per 3 bytes, and the prefix is the longest of the
855
+ * four media types' spellings. */
856
+ const MAX_ICON_DATA_URL_LENGTH = 26 + Math.ceil(32768 / 3) * 4;
857
+ /** The four types a catalogue icon can have (design §6.1); nothing else
858
+ * reaches an `img.src`. */
859
+ const ICON_DATA_URL = /^data:image\/(?:svg\+xml|png|jpeg|webp);base64,[A-Za-z0-9+/]+={0,2}$/;
860
+ /**
861
+ * Read and verify an icons sidecar; ANY irregularity degrades to no icons —
862
+ * icons are presentation, and they never cost the reader the catalog (§7).
863
+ * The checks are the grammar the build writes: a 64-hex sha256 keying a
864
+ * `data:` URL of the four types, each within the per-icon bound.
865
+ */
866
+ function parseIconsText(text) {
867
+ try {
868
+ const parsed = JSON.parse(text);
869
+ if (typeof parsed.icons !== "object" || parsed.icons === null || Array.isArray(parsed.icons)) return {};
870
+ const out = Object.create(null);
871
+ for (const [key, value] of Object.entries(parsed.icons)) {
872
+ if (!/^[0-9a-f]{64}$/.test(key)) return {};
873
+ if (typeof value !== "string" || value.length > MAX_ICON_DATA_URL_LENGTH || !ICON_DATA_URL.test(value)) return {};
874
+ out[key] = value;
875
+ }
876
+ return out;
877
+ } catch {
878
+ return {};
879
+ }
880
+ }
760
881
  /**
761
882
  * Load the catalog snapshot: fetch the pointer, verify the data file's sha256
762
883
  * against it, cache both on disk, and serve the cached copy with `stale: true`
@@ -806,13 +927,19 @@ async function loadCatalog(options) {
806
927
  const starsText = fsImpl.read(cachePath(cacheDir, pointer.stars.url, "stars"));
807
928
  if (createHash("sha256").update(starsText).digest("hex") === pointer.stars.sha256) stars = parseStarsText(starsText);
808
929
  } catch {}
930
+ let icons = {};
931
+ if (pointer.icons !== void 0) try {
932
+ const iconsText = fsImpl.read(cachePath(cacheDir, pointer.icons.url, "icons"));
933
+ if (createHash("sha256").update(iconsText).digest("hex") === pointer.icons.sha256) icons = parseIconsText(iconsText);
934
+ } catch {}
809
935
  return {
810
936
  schemaVersion: pointer.schemaVersion,
811
937
  builtAt: pointer.builtAt,
812
938
  entries: data.plugins,
813
939
  denied: data.denied,
814
940
  notAShop: data.notAShop,
815
- stars
941
+ stars,
942
+ icons
816
943
  };
817
944
  };
818
945
  if (!refresh && fsImpl.exists(indexPath)) {
@@ -879,13 +1006,26 @@ async function loadCatalog(options) {
879
1006
  }
880
1007
  } catch {}
881
1008
  }
1009
+ let icons = {};
1010
+ if (pointer.icons !== void 0) {
1011
+ const sidecar = pointer.icons;
1012
+ try {
1013
+ const iconsCachePath = cachePath(cacheDir, sidecar.url, "icons");
1014
+ const iconsText = await withCommitBudget(handle.id, `serve ${sidecar.url}`, (signal) => handle.file(sidecar.url, signal));
1015
+ if (createHash("sha256").update(iconsText).digest("hex") === sidecar.sha256) {
1016
+ icons = parseIconsText(iconsText);
1017
+ fsImpl.write(iconsCachePath, iconsText);
1018
+ }
1019
+ } catch {}
1020
+ }
882
1021
  const snapshot = {
883
1022
  schemaVersion: pointer.schemaVersion,
884
1023
  builtAt: pointer.builtAt,
885
1024
  entries: data.plugins,
886
1025
  denied: data.denied,
887
1026
  notAShop: data.notAShop,
888
- stars
1027
+ stars,
1028
+ icons
889
1029
  };
890
1030
  try {
891
1031
  fsImpl.write(dataCachePath, dataText);
@@ -1020,7 +1160,7 @@ function validateInstall(snapshot, args, installedSpec) {
1020
1160
  if (denied !== void 0) return {
1021
1161
  ok: false,
1022
1162
  code: "denied",
1023
- detail: `dsh-plugin-shop: ${args.name} is denied: ${denied.detail}`
1163
+ detail: `dsh-plugin-shop: installation of ${args.name} is blocked: ${denied.detail}`
1024
1164
  };
1025
1165
  let entry;
1026
1166
  if (args.source === void 0) {
@@ -1028,7 +1168,7 @@ function validateInstall(snapshot, args, installedSpec) {
1028
1168
  if (named.length > 1) return {
1029
1169
  ok: false,
1030
1170
  code: "ambiguous-identity",
1031
- detail: `dsh-plugin-shop: the catalog holds ${named.length} entries named ${args.name}, and this request does not say which one; refresh the shop and try again`
1171
+ detail: `dsh-plugin-shop: the catalog has ${named.length} plugins named ${args.name}. Refresh the shop and select the one you want to install.`
1032
1172
  };
1033
1173
  entry = named[0];
1034
1174
  } else {
@@ -1053,7 +1193,7 @@ function validateInstall(snapshot, args, installedSpec) {
1053
1193
  if (entry.version !== args.version) return {
1054
1194
  ok: false,
1055
1195
  code: "version-mismatch",
1056
- detail: `dsh-plugin-shop: ${args.name}@${args.version} is not the cataloged version (${entry.version})`
1196
+ detail: `dsh-plugin-shop: you requested ${args.name}@${args.version}, but the catalog lists ${entry.version}. Refresh the shop and try again.`
1057
1197
  };
1058
1198
  if (installedSpec !== void 0) {
1059
1199
  const verdict = specVerdict(entry, installedSpec);
@@ -1062,14 +1202,14 @@ function validateInstall(snapshot, args, installedSpec) {
1062
1202
  return {
1063
1203
  ok: false,
1064
1204
  code: "name-taken",
1065
- detail: `${verdict === "unknown" ? `dsh-plugin-shop: ${args.name} is already installed from ${holder}, which the shop cannot identify as any catalog entry.` : `dsh-plugin-shop: ${args.name} is already installed from ${holder}.`} A profile holds one dependency per name, so installing this one would overwrite it and the plugin you have would be gone. Uninstall it first if you mean to switch.`
1205
+ detail: `${verdict === "unknown" ? `dsh-plugin-shop: ${args.name} is already installed from ${holder}. The shop could not match it to a catalog entry.` : `dsh-plugin-shop: ${args.name} is already installed from ${holder}.`} Installing this plugin would replace it, since a profile can hold only one plugin of each name. To switch, uninstall the existing plugin first.`
1066
1206
  };
1067
1207
  }
1068
1208
  }
1069
1209
  if (entry.tier !== "verified" && !args.acknowledged) return {
1070
1210
  ok: false,
1071
1211
  code: "needs-acknowledgement",
1072
- detail: entry.tier === "verified-stale" ? `dsh-plugin-shop: ${args.name} is verified-stale: a newer version than the review is current and has not been reviewed; acknowledgement is required` : `dsh-plugin-shop: ${args.name} is ${entry.tier}-tier and has not been reviewed; acknowledgement is required`
1212
+ detail: entry.tier === "verified-stale" ? `dsh-plugin-shop: a different version of ${args.name} was reviewed. The current version has not been reviewed. Confirm the installation to continue.` : `dsh-plugin-shop: ${args.name} is a community plugin and has not been reviewed. Confirm the installation to continue.`
1073
1213
  };
1074
1214
  return {
1075
1215
  ok: true,
@@ -1632,11 +1772,11 @@ function createBoundedLog() {
1632
1772
  */
1633
1773
  function activationFailureDetail(args) {
1634
1774
  const { expectedName, profile, before, after } = args;
1635
- if (Object.hasOwn(after, expectedName)) return `${expectedName} is a dependency of the profile but is not in dsh.profile.bundles, so dsh did not activate it as a profile layer and the shop has nothing to mount.`;
1636
- if (before === null) return `${expectedName} is in neither dsh.profile.bundles nor the profile's dependencies, and the profile manifest could not be read before the install, so what the install changed is unknown — compare the profile's package.json against what you expected before retrying.`;
1775
+ if (Object.hasOwn(after, expectedName)) return `${expectedName} is installed as a profile dependency, but is not listed in dsh.profile.bundles. dsh has not enabled it as a plugin, so the shop cannot load it.`;
1776
+ if (before === null) return `${expectedName} is missing from both dsh.profile.bundles and the profile dependencies. The shop could not read the profile before installation, so it cannot tell what changed. Check the profile's package.json before trying again.`;
1637
1777
  const added = Object.keys(after).filter((name) => !Object.hasOwn(before, name)).sort();
1638
- if (added.length > 0) return `${expectedName} is not in the profile's dependencies — the install added ${added.join(", ")} instead. Remove it with: dsh plugin --profile ${profile} remove ${added[0]}`;
1639
- return `${expectedName} is in neither dsh.profile.bundles nor the profile's dependencies, and the install added nothing — if the entry is new the catalog may be behind; refresh it and retry.`;
1778
+ if (added.length > 0) return `${expectedName} is not in the profile dependencies. The install added ${added.join(", ")} instead. To remove ${added[0]}, run: dsh plugin --profile ${profile} remove ${added[0]}`;
1779
+ return `${expectedName} is missing from both dsh.profile.bundles and the profile dependencies. Nothing was added. If this plugin was recently published, the catalog may be out of date. Refresh it and try again.`;
1640
1780
  }
1641
1781
  /** The §7.2 step-6 confirm: after a zero exit, re-read the profile manifest and
1642
1782
  * verify the bundle actually landed in `dsh.profile.bundles`. Exit 0 alone is
@@ -1663,7 +1803,7 @@ function confirmBundleActivation(profile, home, expectedName, before) {
1663
1803
  after
1664
1804
  });
1665
1805
  } catch {
1666
- return `installed but the profile manifest could not be read (${join(profileDir, "package.json")}) — the install's result is unknown; check that file.`;
1806
+ return `Could not confirm the installation because the profile manifest could not be read (${join(profileDir, "package.json")}). Check that file before trying again.`;
1667
1807
  }
1668
1808
  }
1669
1809
  /** The uninstall mirror of §7.2 step 6: after a zero exit, re-read the profile
@@ -1673,9 +1813,9 @@ function confirmBundleRemoval(profile, home, expectedName) {
1673
1813
  const profileDir = resolveProfileDir(profile, home);
1674
1814
  try {
1675
1815
  if (!readProfileManifest("dsh-plugin-shop", profileDir).dsh?.profile?.bundles?.includes(expectedName)) return null;
1676
- return "removed but dsh.profile.bundles did not change — re-run the uninstall";
1816
+ return "The uninstall command finished, but the plugin is still listed in dsh.profile.bundles. Try uninstalling it again.";
1677
1817
  } catch {
1678
- return `removed but the profile manifest could not be read (${join(profileDir, "package.json")}) — re-run the uninstall`;
1818
+ return `Could not confirm the uninstall because the profile manifest could not be read (${join(profileDir, "package.json")}). Check that file and try again.`;
1679
1819
  }
1680
1820
  }
1681
1821
  /** Lines a failed install's log tends to END on, none of which tell the user
@@ -1783,7 +1923,7 @@ const NATIVE_ALLOCATION_ABORT = /^memory allocation of \d+ bytes failed$/;
1783
1923
  function nativeAllocationAbortDetail(usable) {
1784
1924
  const abort = usable.find((line) => NATIVE_ALLOCATION_ABORT.test(line.trim()));
1785
1925
  if (abort === void 0) return null;
1786
- return `pnpm crashed: its native binary ran out of memory (${abort.trim()}), which is not caused by this plugin. pnpm 12 before 12.7.0 has a known bug that exhausts memory when many packages share a missing peer, which every dsh profile has, because dsh keeps peers uninstalled (autoInstallPeers: false; pnpm/pnpm#15362). Upgrade pnpm to 12.7.0 or later, or use pnpm 11, then install again. If your pnpm is already 12.7.0 or later, report the line above to pnpm.`;
1926
+ return `pnpm ran out of memory (${abort.trim()}). pnpm 12 versions before 12.7.0 have a known memory bug with shared missing peer dependencies (pnpm/pnpm#15362). Upgrade pnpm to 12.7.0 or later, or use pnpm 11, then install again. If your pnpm is already 12.7.0 or later, report the line above to pnpm.`;
1787
1927
  }
1788
1928
  /**
1789
1929
  * The one log line worth putting in front of the user, plus the recovery hint.
@@ -1825,13 +1965,13 @@ function installFailureDetail(profile, log, hint = `pnpm failed in the profile.
1825
1965
  * 2026-09-02 as "Update failed / dsh not found on PATH" on a working install.
1826
1966
  */
1827
1967
  function spawnFailureDetail(code, message, dshBin, platform) {
1828
- if (platform === "win32" && (code === "ENOENT" || code === "EINVAL")) return `the shop could not locate the dsh CLI to run (${dshBin}): on Windows npm installs it as a .cmd shim, which cannot be spawned directly, so the shop runs the ${DSH_PACKAGE} package's own entry through node instead — and that entry was not found. Check that \`dsh --version\` works in a terminal, then reinstall the CLI if it does not.`;
1829
- if (code === "ENOENT") return "dsh not found on PATH — install the dsh CLI to manage profile plugins";
1830
- return `dsh spawn failed: ${message}`;
1968
+ if (platform === "win32" && (code === "ENOENT" || code === "EINVAL")) return `The shop could not find the dsh CLI entry file (${DSH_PACKAGE}, command: ${dshBin}). On Windows, the shop needs this file because it cannot run the .cmd launcher directly. Check that \`dsh --version\` works in a terminal, then reinstall the CLI if it does not.`;
1969
+ if (code === "ENOENT") return "dsh was not found on PATH. Install the dsh CLI or add it to PATH, then try again.";
1970
+ return `Could not start dsh: ${message}`;
1831
1971
  }
1832
1972
  /** The failure detail for a command stopped by the shop's deadline. */
1833
1973
  function installTimeoutDetail(profile, timeoutMs) {
1834
- return `dsh-plugin-shop: the command did not finish within ${Math.max(1, Math.round(timeoutMs / 1e3))}s and was stopped. Run it yourself to see what it is waiting on: dsh plugin --profile ${profile} install`;
1974
+ return `dsh-plugin-shop: the command did not finish within ${Math.max(1, Math.round(timeoutMs / 1e3))}s and was stopped. To check the error and retry, run: dsh plugin --profile ${profile} install`;
1835
1975
  }
1836
1976
  /** The real kill. Exported for the tests that inject a RECORDING `KillFns`:
1837
1977
  * a recorder that only records asserts that a kill was requested and leaves
@@ -4253,33 +4393,33 @@ function thrownTail(message, desktop) {
4253
4393
  /** What each management code means, in dsh 0.1.7-rc.2's own terms (the
4254
4394
  * implementation plan of 2026-09-27, Task 2, records where each is raised). */
4255
4395
  const CODE_SENTENCE = {
4256
- "unknown-plugin": "dsh lists no plugin entry by that id",
4257
- "invalid-spec": "dsh could not read the install spec",
4258
- "ambiguous-install": "dsh could not tell which package the install added",
4396
+ "unknown-plugin": "dsh could not find a plugin with that ID",
4397
+ "invalid-spec": "dsh could not read the package name or install source",
4398
+ "ambiguous-install": "dsh could not identify the installed package",
4259
4399
  "not-bundle": "the package is not a dsh bundle",
4260
4400
  "not-removable": "dsh does not allow removing this bundle",
4261
- "stop-profile": "the bundle is running and this dsh cannot unload it live, so the profile has to be stopped first",
4401
+ "stop-profile": "this bundle is still running; stop dsh for this profile before removing it",
4262
4402
  "bundle-in-use": "the bundle was switched off, but some of its plugins are still running",
4263
- "stale-approval": "a build approval names a package pnpm no longer holds",
4403
+ "stale-approval": "the build approval refers to a package that pnpm no longer lists",
4264
4404
  "management-required": "the plugin belongs to dsh itself",
4265
- unaddressable: "the plugin is not a row of the profile's own patch, so dsh cannot address it",
4266
- "operation-error": "dsh hit an unexpected error",
4267
- "already-installed": "this dsh updates no installed package in place: uninstall it, then install it again"
4405
+ unaddressable: "the plugin is not defined in this profile's configuration, so dsh cannot change it here",
4406
+ "operation-error": "dsh encountered an unexpected error",
4407
+ "already-installed": "this version of dsh cannot update an installed package directly; uninstall it, then install it again"
4268
4408
  };
4269
4409
  /** What each classified pnpm failure was, as dsh classifies it. */
4270
4410
  const KIND_SENTENCE = {
4271
4411
  "pnpm-missing": "pnpm could not be started",
4272
- timeout: "pnpm did not finish within dsh's own time bound",
4273
- "not-found": "no such package was found",
4274
- "no-matching-version": "no version matching the requested one was found",
4275
- network: "the network failed",
4412
+ timeout: "pnpm took too long to finish",
4413
+ "not-found": "the package could not be found",
4414
+ "no-matching-version": "the requested version could not be found",
4415
+ network: "the network request failed",
4276
4416
  "disk-full": "the disk is full",
4277
4417
  permission: "permission was denied",
4278
4418
  integrity: "the downloaded package failed its integrity check"
4279
4419
  };
4280
4420
  const WHERE = {
4281
4421
  registry: " at the registry",
4282
- "spec-host": " at the host the package is fetched from"
4422
+ "spec-host": " while connecting to the package source"
4283
4423
  };
4284
4424
  /** An own-property read of one of the three tables above. They are object
4285
4425
  * literals, so a bare index read answers for Object.prototype, and a code of
@@ -4306,10 +4446,10 @@ function restorationSentence(output) {
4306
4446
  * so its restoration line is passed on. Without one, an install or update
4307
4447
  * says what dsh's own pre-check says, and an uninstall says nothing of it. */
4308
4448
  function versionRefusalDetail(context, change) {
4309
- const refused = change.incompatible.map((issue) => `${issue.name}@${issue.version} declares ${Object.entries(issue.peers).map(([peer, range]) => `${peer} ${range}`).join(", ")}, which dsh ${issue.runtimeVersion} does not satisfy`);
4449
+ const refused = change.incompatible.map((issue) => `${issue.name}@${issue.version} requires ${Object.entries(issue.peers).map(([peer, range]) => `${peer} ${range}`).join(", ")}, which dsh ${issue.runtimeVersion} does not satisfy`);
4310
4450
  const restored = restorationSentence(change.output) ?? (context.operation === "uninstall" || change.truncated ? null : "Nothing was installed.");
4311
4451
  const base = `dsh-plugin-shop: dsh refused the ${context.operation}: ${refused.join("; ")}.${restored === null ? "" : ` ${restored}`}`;
4312
- if (context.desktop) return `${base} dsh's CLI, which grants version exemptions, does not manage the desktop profile, and this shop grants none.`;
4452
+ if (context.desktop) return `${base} Version exemptions are not available here: the dsh CLI cannot manage desktop profiles, and the shop cannot grant them.`;
4313
4453
  const commands = change.incompatible.map((issue) => allowVersionCommand(context.profile, issue)).filter((command) => command !== null);
4314
4454
  if (commands.length === 0) return base;
4315
4455
  return `${base} To accept the risk of crashes or data loss for ${commands.length === 1 ? "this exact version" : "these exact versions"}, run: ${commands.join("; ")} - then ${context.operation} again.`;
@@ -4334,9 +4474,9 @@ function codeReason(change, code) {
4334
4474
  * Add plugin dialog refuses a name the profile already holds, so it cannot
4335
4475
  * rerun an update, and with no name listed it offers no such button. */
4336
4476
  function desktopBuildsDetail(context, change, held) {
4337
- const base = `dsh-plugin-shop: pnpm is holding the build scripts of ${held}, which it blocks by default, and this shop never allows them.`;
4477
+ const base = `dsh-plugin-shop: pnpm blocked build scripts for ${held}. These scripts need approval, which the shop cannot grant.`;
4338
4478
  if (context.operation !== "install" || change.pendingBuilds.length === 0) return base;
4339
- return `${base} dsh's Plugins page offers "Allow these scripts and retry" when an install it runs stops on them.`;
4479
+ return `${base} Try installing from dsh's Plugins page. If these scripts block the install there, use "Allow these scripts and retry" to approve them.`;
4340
4480
  }
4341
4481
  /** What a `cancelled` answer means, for every reader: the shop's deadline
4342
4482
  * asked dsh to stop the operation, and dsh answers `cancelled` only once its
@@ -4349,7 +4489,7 @@ function desktopBuildsDetail(context, change, held) {
4349
4489
  * update. */
4350
4490
  function cancelledDetail(context) {
4351
4491
  const seconds = Math.max(1, Math.round(context.timeoutMs / 1e3));
4352
- return `dsh-plugin-shop: the ${context.operation} did not finish within ${seconds}s, so the shop cancelled it. dsh stopped it, restored the profile's package.json and pnpm-lock.yaml, and installed nothing. Try the ${context.operation} again from the shop.`;
4492
+ return `dsh-plugin-shop: the ${context.operation} did not finish within ${seconds}s, so the shop cancelled it. dsh restored the profile's package.json and pnpm-lock.yaml. Nothing was installed. Try the ${context.operation} again from the shop.`;
4353
4493
  }
4354
4494
  /** A `ChangeResult` as the shop's terminal install record: design
4355
4495
  * 2026-09-26-plugin-manager-delegation, section 5, read by the first rule
@@ -4395,12 +4535,12 @@ function outcomeByRule(change, context) {
4395
4535
  };
4396
4536
  return {
4397
4537
  state: "failed",
4398
- detail: `dsh-plugin-shop: pnpm is holding the build scripts of ${held}, which it blocks by default: run \`pnpm approve-builds\` in the profile directory to allow them, then ${context.operation} again.`
4538
+ detail: `dsh-plugin-shop: pnpm blocked build scripts for ${held}. To review and allow them, run \`pnpm approve-builds\` in the profile directory, then ${context.operation} again.`
4399
4539
  };
4400
4540
  }
4401
4541
  if (change.kind === "timeout" && change.failedAt === "spec-host") return {
4402
4542
  state: "failed",
4403
- detail: `dsh-plugin-shop: the ${context.operation} failed at the host the package is fetched from: dsh checks that a GitHub repository is reachable before pnpm runs, and that check did not finish within its bound (5 s by default), so pnpm never ran.`
4543
+ detail: `dsh-plugin-shop: the ${context.operation} stopped because the GitHub connection check timed out (5 s by default). pnpm has not run. Check your connection to GitHub and try again.`
4404
4544
  };
4405
4545
  const sentence = lookup(KIND_SENTENCE, change.kind);
4406
4546
  if (sentence !== void 0) {
@@ -4433,7 +4573,7 @@ function outcomeByRule(change, context) {
4433
4573
  activation: "restart"
4434
4574
  };
4435
4575
  if (change.application === "applied" || change.application === "overridden") {
4436
- const note = change.application === "overridden" ? { detail: "Saved, but a higher-priority layer (the home or invocation patch) decides whether it runs." } : {};
4576
+ const note = change.application === "overridden" ? { detail: "Saved, but a home or startup configuration overrides this setting and controls whether the plugin runs." } : {};
4437
4577
  if (context.operation !== "uninstall" && context.alreadyImported && !(change.application === "applied" && context.reconcilesRunningPlugins)) return {
4438
4578
  state: "done",
4439
4579
  activation: "restart",
@@ -4452,7 +4592,7 @@ function outcomeByRule(change, context) {
4452
4592
  }
4453
4593
  return {
4454
4594
  state: "failed",
4455
- detail: `dsh-plugin-shop: dsh answered the ${context.operation} with "${change.application ?? "nothing"}", which this shop does not know how to read.`
4595
+ detail: `dsh-plugin-shop: could not confirm the ${context.operation}. dsh returned an unrecognized status: "${change.application ?? "nothing"}".`
4456
4596
  };
4457
4597
  }
4458
4598
  //#endregion
@@ -4565,7 +4705,7 @@ function startManagerOperation(options) {
4565
4705
  } catch (error) {
4566
4706
  settled = {
4567
4707
  state: "failed",
4568
- detail: `dsh-plugin-shop: the shop could not read dsh's answer: ${messageOf(error)}`
4708
+ detail: `dsh-plugin-shop: could not read the result from dsh: ${messageOf(error)}`
4569
4709
  };
4570
4710
  state = "failed";
4571
4711
  return status();
@@ -4579,7 +4719,7 @@ function startManagerOperation(options) {
4579
4719
  } catch (error) {
4580
4720
  result = {
4581
4721
  state: "failed",
4582
- detail: `dsh-plugin-shop: the shop could not check what dsh installed: ${messageOf(error)}`
4722
+ detail: `dsh-plugin-shop: could not verify the installed plugin: ${messageOf(error)}`
4583
4723
  };
4584
4724
  }
4585
4725
  settled = result;
@@ -4741,17 +4881,17 @@ var __esDecorate = function(ctor, descriptorIn, decorators, contextIn, initializ
4741
4881
  * service and to set an override that the platform check, being checked ahead
4742
4882
  * of the systemd one, could never reach. */
4743
4883
  const RESTART_BLOCKED_DETAIL = {
4744
- desktop: "dsh-plugin-shop: the desktop profile is managed by the DeepSeek Harness desktop app, and dsh refuses to restart it from here; restart the app to apply the change",
4745
- "from-default-profile": "dsh-plugin-shop: restart is not supported when dsh was launched with --from-default-profile; from dsh 0.2.1 a restart would recreate the profile from its template and delete every plugin installed in it, this shop included. Restart dsh manually without that flag to apply the change",
4746
- windows: "dsh-plugin-shop: restart is not supported on Windows yet; restart dsh manually to apply the change",
4747
- systemd: "dsh-plugin-shop: restart is disabled because this process is a systemd service — a restart would kill the takeover helper along with the unit, and the service would not come back. Set allowRestart: true in the shop row config to override.",
4748
- "port-zero": "dsh-plugin-shop: restart is not supported when dsh was launched with --port 0; restart dsh manually"
4884
+ desktop: "dsh-plugin-shop: this profile is managed by the DeepSeek Harness desktop app. Restart the app to apply the change.",
4885
+ "from-default-profile": "dsh-plugin-shop: dsh was started with --from-default-profile. In dsh 0.2.1 and later, restarting with this flag recreates the profile from its template and deletes all installed plugins, including the shop. Restart dsh manually without this flag.",
4886
+ windows: "dsh-plugin-shop: restarting from the shop is not supported on Windows yet. Quit dsh and start it again to apply the change.",
4887
+ systemd: "dsh-plugin-shop: dsh is managed by systemd. Restart the service manually; restarting from the shop could stop the service without bringing it back. To allow restarts from the shop, set allowRestart: true in the shop configuration.",
4888
+ "port-zero": "dsh-plugin-shop: dsh was started with --port 0. Its port will change after a restart. Restart dsh manually and open the new address."
4749
4889
  };
4750
4890
  /** Why the shop changes nothing in the desktop profile, for every mutation
4751
4891
  * that spawns dsh's CLI: install, uninstall and self-update. dsh refuses
4752
4892
  * the profile before any of them would run (`isDesktopProfile`), and the
4753
4893
  * failure that surfaced instead read "pnpm failed in the profile". */
4754
- const DESKTOP_PROFILE_DETAIL = "dsh-plugin-shop: the desktop profile is managed by the DeepSeek Harness desktop app, and dsh refuses to change it from the command line the shop runs; add and remove its plugins from the app instead";
4894
+ const DESKTOP_PROFILE_DETAIL = "dsh-plugin-shop: this profile is managed by the DeepSeek Harness desktop app. The shop cannot change it through the command line. Add or remove plugins from the app.";
4755
4895
  /** Why a switch through dsh's `pluginManager` waits for an operation this
4756
4896
  * gateway started: dsh holds its profile lock for a whole install or removal
4757
4897
  * (`change()` wraps pnpm) and makes any other change wait for it up to its
@@ -4760,14 +4900,14 @@ const DESKTOP_PROFILE_DETAIL = "dsh-plugin-shop: the desktop profile is managed
4760
4900
  * The shop's own row writer takes no dsh lock, so what it writes alone (the
4761
4901
  * CLI path's switch, and switching a deselected package off) is not
4762
4902
  * refused. */
4763
- const switchWhileBusyDetail = (name) => `dsh-plugin-shop: an install, update or uninstall is still running in this profile, and dsh holds the profile until it ends; switch ${name} after it finishes.`;
4903
+ const switchWhileBusyDetail = (name) => `dsh-plugin-shop: an install, update or uninstall is still running in this profile. Wait for it to finish before changing the enabled state of ${name}.`;
4764
4904
  /** The line a record gains when dsh's plugin manager refuses an update in
4765
4905
  * place and the shop continues it through dsh's CLI (design
4766
4906
  * 2026-10-19-dsh-021-readiness §2): what dsh refused, and the command that
4767
4907
  * runs instead. It names a `dsh plugin` command, so it never reaches a
4768
4908
  * desktop reader: the continuation does not run in that profile. */
4769
4909
  function cliFallbackLine(profile, spec) {
4770
- return `dsh-plugin-shop: dsh's plugin manager updates no installed package in place (already-installed); running \`dsh plugin --profile ${profile} add ${spec}\` instead`;
4910
+ return `dsh-plugin-shop: dsh's plugin manager cannot update this package directly (already-installed). Continuing with: \`dsh plugin --profile ${profile} add ${spec}\``;
4771
4911
  }
4772
4912
  /** How many bytes a release tarball may be at the integrity check. The
4773
4913
  * registry already refuses to publish a tarball over 32 MiB, so 64 MiB is
@@ -4787,10 +4927,10 @@ async function verifyTarballSha256(fetchTarball, url, recordedSha256, maxBytes =
4787
4927
  try {
4788
4928
  response = await fetchTarball(url);
4789
4929
  } catch (error) {
4790
- return `dsh-plugin-shop: the release tarball could not be fetched (network failure: ${error.message}); refusing to install`;
4930
+ return `dsh-plugin-shop: could not download the release archive. Installation stopped. Network error: ${error.message}`;
4791
4931
  }
4792
- if (!response.ok) return `dsh-plugin-shop: the release tarball could not be fetched (HTTP ${response.status}); refusing to install`;
4793
- if (response.body === null) return "dsh-plugin-shop: the release tarball has no readable body; refusing to install";
4932
+ if (!response.ok) return `dsh-plugin-shop: could not download the release archive (HTTP ${response.status}). Installation stopped.`;
4933
+ if (response.body === null) return "dsh-plugin-shop: the release archive response was empty or unreadable. Installation stopped.";
4794
4934
  const hash = createHash("sha256");
4795
4935
  let bytes = 0;
4796
4936
  const reader = response.body.getReader();
@@ -4803,14 +4943,14 @@ async function verifyTarballSha256(fetchTarball, url, recordedSha256, maxBytes =
4803
4943
  try {
4804
4944
  await reader.cancel();
4805
4945
  } catch {}
4806
- return `dsh-plugin-shop: the release tarball exceeds the size cap (${maxBytes} bytes); refusing to install`;
4946
+ return `dsh-plugin-shop: the release archive exceeds the size limit (${maxBytes} bytes). Installation stopped.`;
4807
4947
  }
4808
4948
  hash.update(value);
4809
4949
  }
4810
4950
  } catch (error) {
4811
- return `dsh-plugin-shop: the release tarball download failed (${error.message}); refusing to install`;
4951
+ return `dsh-plugin-shop: the release archive download failed. Installation stopped. Error: ${error.message}`;
4812
4952
  }
4813
- if (hash.digest("hex") !== recordedSha256) return "dsh-plugin-shop: the release tarball failed sha256 verification against the catalog record; refusing to install";
4953
+ if (hash.digest("hex") !== recordedSha256) return "dsh-plugin-shop: the release archive does not match the catalog checksum (SHA-256). Installation stopped.";
4814
4954
  return null;
4815
4955
  }
4816
4956
  /** An own-property read of a dependency map parsed from the profile manifest.
@@ -5381,7 +5521,7 @@ let ShopGateway = (() => {
5381
5521
  async setEnabled(args) {
5382
5522
  if (args.name === "dsh-plugin-shop" || args.name.startsWith("@deepseek-ai/")) return {
5383
5523
  ok: false,
5384
- detail: `dsh-plugin-shop: ${args.name} is part of the harness chain and cannot be toggled from the shop`
5524
+ detail: `dsh-plugin-shop: ${args.name} is part of dsh itself and cannot be enabled or disabled from the shop.`
5385
5525
  };
5386
5526
  const manager = this.pluginManager();
5387
5527
  const profileDir = this.profileDirResolved();
@@ -5399,12 +5539,12 @@ let ShopGateway = (() => {
5399
5539
  } catch (error) {
5400
5540
  return {
5401
5541
  ok: false,
5402
- detail: `dsh-plugin-shop: ${args.name} has a bundle patch that could not be read: ${String(error)}`
5542
+ detail: `dsh-plugin-shop: could not read the plugin configuration for ${args.name}: ${String(error)}`
5403
5543
  };
5404
5544
  }
5405
5545
  if (owned.length === 0) return {
5406
5546
  ok: false,
5407
- detail: `dsh-plugin-shop: ${args.name} contributes no plugin entries, so there is nothing to enable or disable`
5547
+ detail: `dsh-plugin-shop: ${args.name} has no plugin entries to enable or disable.`
5408
5548
  };
5409
5549
  const selected = this.runningProfileBundles(profileDir);
5410
5550
  if (selected !== null && !selected.includes(args.name)) return this.setDeselectedEnabled(args.name, args.enabled, profileDir, owned, manager);
@@ -5412,7 +5552,7 @@ let ShopGateway = (() => {
5412
5552
  const live = (await this.listInventory()).filter((entry) => ownsEntryId(ownedSet, entry.entryId));
5413
5553
  if (live.length === 0) return {
5414
5554
  ok: false,
5415
- detail: `dsh-plugin-shop: ${args.name} is installed but its entries are not in the running plugin tree; restart dsh to compose them`
5555
+ detail: `dsh-plugin-shop: ${args.name} is installed but has not been loaded. Restart dsh to load it.`
5416
5556
  };
5417
5557
  if (manager !== null) {
5418
5558
  if (this.hasRunningCommand()) return {
@@ -5508,7 +5648,7 @@ let ShopGateway = (() => {
5508
5648
  }
5509
5649
  if (manager === null) return {
5510
5650
  ok: false,
5511
- detail: `dsh-plugin-shop: ${name} is installed but switched off at the bundle level: dsh.profile.bundles in ${join(profileDir, "package.json")} does not list it, so dsh composes none of it, and a restart will not change that. This dsh has no pluginManager service to select it again: add "${name}" back to that list and restart dsh, or uninstall it and install it again from the shop.`
5651
+ detail: `dsh-plugin-shop: ${name} is installed but disabled. It is missing from dsh.profile.bundles in ${join(profileDir, "package.json")}, so restarting alone will not enable it. This version of dsh cannot enable the bundle from the shop. Add "${name}" back to that list and restart dsh, or uninstall it and install it again from the shop.`
5512
5652
  };
5513
5653
  if (this.hasRunningCommand()) return {
5514
5654
  ok: false,
@@ -5520,7 +5660,7 @@ let ShopGateway = (() => {
5520
5660
  } catch (error) {
5521
5661
  return {
5522
5662
  ok: false,
5523
- detail: `dsh-plugin-shop: dsh could not select ${name} again${thrownTail(messageOf(error), isDesktopProfile(this.profile))}`
5663
+ detail: `dsh-plugin-shop: dsh could not enable ${name} again${thrownTail(messageOf(error), isDesktopProfile(this.profile))}`
5524
5664
  };
5525
5665
  }
5526
5666
  const { change, failure } = this.readManagerAnswer(raw, {
@@ -5579,7 +5719,7 @@ let ShopGateway = (() => {
5579
5719
  }
5580
5720
  if (change.application === "overridden") return {
5581
5721
  change,
5582
- failure: `dsh-plugin-shop: dsh saved ${words.request}, but a home or invocation patch, which outranks the profile's own, keeps it ${words.stays}.`
5722
+ failure: `dsh-plugin-shop: dsh saved ${words.request}, but a home or startup configuration overrides it. The plugin remains ${words.stays}.`
5583
5723
  };
5584
5724
  if (change.application === "applied" || change.application === "restart-required") return {
5585
5725
  change,
@@ -5587,7 +5727,7 @@ let ShopGateway = (() => {
5587
5727
  };
5588
5728
  return {
5589
5729
  change,
5590
- failure: `dsh-plugin-shop: dsh answered ${words.request} with "${change.application ?? "nothing"}", which this shop does not know how to read.`
5730
+ failure: `dsh-plugin-shop: could not confirm ${words.request}. dsh returned an unrecognized status: "${change.application ?? "nothing"}".`
5591
5731
  };
5592
5732
  }
5593
5733
  rowConfig() {
@@ -5756,6 +5896,7 @@ let ShopGateway = (() => {
5756
5896
  denied: snapshot.denied,
5757
5897
  notAShop: snapshot.notAShop ?? [],
5758
5898
  stars: snapshot.stars,
5899
+ icons: snapshot.icons ?? {},
5759
5900
  incompatible,
5760
5901
  incompatibleHarness
5761
5902
  };
@@ -5915,14 +6056,14 @@ let ShopGateway = (() => {
5915
6056
  packageName: name,
5916
6057
  dependencies: Object.keys(this.profileDependenciesOrNone() ?? {})
5917
6058
  });
5918
- if (clash !== null) return `dsh-plugin-shop: ${name} declares the loader entry id "${clash.id}", which ${clash.holder} already declares. dsh refuses to load a plugin tree holding a duplicate entry id, so the profile would not start.` + undo;
6059
+ if (clash !== null) return `dsh-plugin-shop: ${name} and ${clash.holder} use the same plugin entry ID, "${clash.id}". dsh cannot start this profile while both plugins are installed.` + undo;
5919
6060
  switch (patchDeclarationHazard(declaredBundlePatch({
5920
6061
  profileDir: this.profileDirResolved(),
5921
6062
  packageName: name
5922
6063
  }), harness.patchLists)) {
5923
6064
  case null: return null;
5924
- case "list-unsupported": return `dsh-plugin-shop: ${name} lists its bundle patch as several files, which dsh reads from 0.1.7 on; this dsh${harness.dshVersion === null ? "" : ` (${harness.dshVersion})`} reads one and would not start with it installed.` + undo;
5925
- case "malformed": return `dsh-plugin-shop: ${name} declares its bundle patch as neither a file path nor a list of them, which dsh refuses to load, so the profile would not start.` + undo;
6065
+ case "list-unsupported": return `dsh-plugin-shop: ${name} uses multiple configuration files, which require dsh 0.1.7 or later. Your dsh${harness.dshVersion === null ? "" : ` (${harness.dshVersion})`} only supports one file and cannot start with this plugin installed.` + undo;
6066
+ case "malformed": return `dsh-plugin-shop: ${name} has an invalid bundle patch declaration; it must be a file path or a list of paths. dsh cannot start this profile with the plugin installed.` + undo;
5926
6067
  }
5927
6068
  }
5928
6069
  /** Remember a github entry's pinned commit: the manifest records only
@@ -6030,7 +6171,7 @@ let ShopGateway = (() => {
6030
6171
  found: false,
6031
6172
  state: "failed",
6032
6173
  log: [],
6033
- detail: `unknown installId: ${args.installId}`
6174
+ detail: `The installation record (${args.installId}) is no longer available. Refresh the shop to check whether the plugin was installed.`
6034
6175
  };
6035
6176
  return {
6036
6177
  found: true,
@@ -6238,7 +6379,7 @@ let ShopGateway = (() => {
6238
6379
  if (this.committedRestart !== null) return this.committedRestart;
6239
6380
  if (this.hasRunningCommand()) return {
6240
6381
  ok: false,
6241
- detail: "dsh-plugin-shop: an install is still running in this profile; a restart now would boot the new dsh against a half-written profile. Wait for it to finish and try again."
6382
+ detail: "dsh-plugin-shop: an install is still running in this profile. Wait for it to finish before restarting so the configuration can be saved completely."
6242
6383
  };
6243
6384
  const blocked = this.staticRestartBlock();
6244
6385
  if (blocked !== null) return {
@@ -6266,7 +6407,7 @@ let ShopGateway = (() => {
6266
6407
  } catch (error) {
6267
6408
  return {
6268
6409
  ok: false,
6269
- detail: `dsh-plugin-shop: restart could not be started: ${error.message}`
6410
+ detail: `dsh-plugin-shop: could not start the restart process: ${error.message}`
6270
6411
  };
6271
6412
  }
6272
6413
  this.committedRestart = {