dsh-plugin-shop 0.5.2 → 0.5.3-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/client.js CHANGED
@@ -4363,7 +4363,14 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4363
4363
  literal(false),
4364
4364
  literal(true)
4365
4365
  ]).optional(),
4366
- "restartReason": union([_undefined(), string()]).optional(),
4366
+ "restartReason": union([
4367
+ _undefined(),
4368
+ literal("no-patch"),
4369
+ literal("not-simple"),
4370
+ literal("host-unsupported"),
4371
+ literal("timeout"),
4372
+ literal("mount-failed")
4373
+ ]).optional(),
4367
4374
  "detail": union([_undefined(), string()]).optional()
4368
4375
  });
4369
4376
  const dsh_plugin_shop_shop_restart_result$schema = union([object({ "ok": literal(true) }), object({
@@ -4427,7 +4434,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4427
4434
  },
4428
4435
  sourceLocation: {
4429
4436
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4430
- "line": 468,
4437
+ "line": 521,
4431
4438
  "column": 9
4432
4439
  }
4433
4440
  },
@@ -4445,7 +4452,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4445
4452
  },
4446
4453
  sourceLocation: {
4447
4454
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4448
- "line": 618,
4455
+ "line": 677,
4449
4456
  "column": 9
4450
4457
  }
4451
4458
  },
@@ -4473,7 +4480,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4473
4480
  },
4474
4481
  sourceLocation: {
4475
4482
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4476
- "line": 494,
4483
+ "line": 547,
4477
4484
  "column": 9
4478
4485
  }
4479
4486
  },
@@ -4500,7 +4507,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4500
4507
  },
4501
4508
  sourceLocation: {
4502
4509
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4503
- "line": 607,
4510
+ "line": 666,
4504
4511
  "column": 3
4505
4512
  }
4506
4513
  },
@@ -4518,7 +4525,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4518
4525
  },
4519
4526
  sourceLocation: {
4520
4527
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4521
- "line": 742,
4528
+ "line": 827,
4522
4529
  "column": 9
4523
4530
  }
4524
4531
  },
@@ -4545,7 +4552,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4545
4552
  },
4546
4553
  sourceLocation: {
4547
4554
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4548
- "line": 421,
4555
+ "line": 442,
4549
4556
  "column": 9
4550
4557
  }
4551
4558
  },
@@ -4573,7 +4580,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4573
4580
  },
4574
4581
  sourceLocation: {
4575
4582
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4576
- "line": 689,
4583
+ "line": 769,
4577
4584
  "column": 9
4578
4585
  }
4579
4586
  },
@@ -4600,7 +4607,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4600
4607
  },
4601
4608
  sourceLocation: {
4602
4609
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4603
- "line": 802,
4610
+ "line": 887,
4604
4611
  "column": 9
4605
4612
  }
4606
4613
  },
@@ -4618,7 +4625,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4618
4625
  },
4619
4626
  sourceLocation: {
4620
4627
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4621
- "line": 786,
4628
+ "line": 871,
4622
4629
  "column": 9
4623
4630
  }
4624
4631
  }
@@ -4626,6 +4633,19 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4626
4633
  };
4627
4634
  //#endregion
4628
4635
  //#region src/client/present.ts
4636
+ /** Hot-mount reason code → locale key, so the notice reads in the language
4637
+ * the person set in dsh. An absent (or unrecognized) reason keeps the generic
4638
+ * restart line rather than showing a bare code. */
4639
+ function restartReasonKey(reason) {
4640
+ switch (reason) {
4641
+ case "no-patch": return "hotNoPatchNotice";
4642
+ case "not-simple": return "hotNotSimpleNotice";
4643
+ case "host-unsupported": return "hotHostUnsupportedNotice";
4644
+ case "timeout": return "hotTimeoutNotice";
4645
+ case "mount-failed": return "hotMountFailedNotice";
4646
+ default: return "installedRestartNotice";
4647
+ }
4648
+ }
4629
4649
  /** Tier → locale key, for the entry-card tier badge (§6.2). */
4630
4650
  function tierKey(tier) {
4631
4651
  switch (tier) {
@@ -4845,6 +4865,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4845
4865
  acknowledgementBody: ACKNOWLEDGEMENT_ZH,
4846
4866
  installing: "正在安装…",
4847
4867
  installedRestartNotice: "已安装;重启 dsh 后生效",
4868
+ hotNoPatchNotice: "该插件没有可热挂载的补丁文件;重启 dsh 后生效",
4869
+ hotNotSimpleNotice: "该插件的补丁包含无法热挂载的配置;重启 dsh 后生效",
4870
+ hotHostUnsupportedNotice: "当前环境不支持热挂载;重启 dsh 后生效",
4871
+ hotTimeoutNotice: "热挂载超时;重启 dsh 后生效",
4872
+ hotMountFailedNotice: "热挂载失败;重启 dsh 后生效",
4848
4873
  installedNoRestartNotice: "已安装,但 profile 未变化——目录可能已过期,请刷新后重试",
4849
4874
  installFailed: "安装失败",
4850
4875
  installTransportFailed: "安装请求未能送达。请稍后重试。",
@@ -4922,6 +4947,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4922
4947
  acknowledgementBody: ACKNOWLEDGEMENT_EN,
4923
4948
  installing: "Installing…",
4924
4949
  installedRestartNotice: "installed; restart dsh to activate",
4950
+ hotNoPatchNotice: "no patch file to hot-mount; restart dsh to activate",
4951
+ hotNotSimpleNotice: "the patch has config rows that cannot be hot-mounted; restart dsh to activate",
4952
+ hotHostUnsupportedNotice: "this harness cannot hot-mount; restart dsh to activate",
4953
+ hotTimeoutNotice: "hot-mount timed out; restart dsh to activate",
4954
+ hotMountFailedNotice: "hot-mount failed; restart dsh to activate",
4925
4955
  installedNoRestartNotice: "installed, but the profile did not change — the catalog may be stale; refresh and try again",
4926
4956
  installFailed: "Install failed",
4927
4957
  installTransportFailed: "The install request could not be delivered. Please retry.",
@@ -5430,7 +5460,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5430
5460
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5431
5461
  className: _dsh_plugin_shop_css_e3675a89_default.notice,
5432
5462
  "data-shop-restart-notice": true,
5433
- children: view.needsRestart ? view.restartReason ?? t("installedRestartNotice") : t("installedNoRestartNotice")
5463
+ children: view.needsRestart ? t(restartReasonKey(view.restartReason)) : t("installedNoRestartNotice")
5434
5464
  }),
5435
5465
  view.needsRestart && restartSupported && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RestartPanel, {
5436
5466
  t,
package/lib/index.js CHANGED
@@ -5,9 +5,9 @@ import { createHash, randomUUID } from "node:crypto";
5
5
  import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
6
6
  import { spawn, spawnSync } from "node:child_process";
7
7
  import { fileURLToPath, pathToFileURL } from "node:url";
8
- import { basename, dirname, join } from "node:path";
8
+ import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
9
9
  import { z } from "zod";
10
- import { dump } from "js-yaml";
10
+ import { JSON_SCHEMA, Type, dump, load } from "js-yaml";
11
11
  //#region src/own-version.ts
12
12
  /** The shop's own published version, read from the package.json that ships
13
13
  * next to this package — the RUNNING version, not the manifest's range
@@ -432,7 +432,7 @@ function spawnPluginCli(options) {
432
432
  restartReason = outcome?.restartReason;
433
433
  } catch {
434
434
  needsRestartOnDone = true;
435
- restartReason = "热挂载失败,重启后生效 / hot-mount failed — restart required";
435
+ restartReason = "mount-failed";
436
436
  }
437
437
  state = "done";
438
438
  } else state = "done";
@@ -504,13 +504,26 @@ function startUninstall(options) {
504
504
  * tree changes back to the file it read (dsh-market hot.ts; the in-tree
505
505
  * precedent is dsh's agent-presets PresetTree).
506
506
  *
507
+ * Reasons are CODES, never copy: the host has no idea which language the
508
+ * person reading it set in dsh, so it names what happened and the client
509
+ * renders it through dsh's own locale service.
510
+ *
507
511
  * Deliberate non-port: dsh-market's client-only shim (`mountClientOnlyDeps`
508
512
  * and `shimNames`, which hot-mounted a package with no server-side entry by
509
513
  * inserting a shim loader entry) is omitted. Our catalog never lists a
510
514
  * package without `dsh.bundle`, so the shim branch is unreachable here
511
515
  * (YAGNI). `hotMount` still distinguishes "no patch file / not
512
- * hot-mountable" from "restart will fix it" through the bilingual `reason`.
516
+ * hot-mountable" from "restart will fix it" through the `reason` code.
513
517
  */
518
+ /** The loader's YAML dialect: `!!js` scalars round-trip as expression nodes
519
+ * rather than throwing, so a patch carrying one is refused for its SHAPE
520
+ * (below) instead of being indistinguishable from malformed YAML. Nothing is
521
+ * evaluated here — the loader evaluates at entry activation. */
522
+ const PATCH_SCHEMA = JSON_SCHEMA.extend(new Type("tag:yaml.org,2002:js", {
523
+ kind: "scalar",
524
+ resolve: () => true,
525
+ construct: (data) => ({ __jsExpr: data })
526
+ }));
514
527
  const nodeFs = {
515
528
  read: (path) => readFileSync(path, "utf8"),
516
529
  write: (path, data) => {
@@ -528,52 +541,43 @@ const HOT_DIR = ".dsh-shop";
528
541
  /** `hot-<n>.yml` files: the only things `cleanHotDir` wipes and the only
529
542
  * files `hotMount` writes. */
530
543
  const HOT_FILE_RE = /^hot-(\d+)\.yml$/;
531
- /** Reasons, bilingual, distinguishing the P0-2 categories: a transient mount
532
- * failure ("restart will fix it") versus a package that structurally cannot
533
- * hot-mount. All tell the user to restart; the head names which category. */
534
- const NO_PATCH_REASON = "该插件没有可热挂载的补丁文件,重启后生效 / the plugin has no patch file to hot-mount — restart required";
535
- const NOT_SIMPLE_REASON = "该插件的补丁包含无法热挂载的配置,重启后生效 / the plugin's patch has config rows that cannot be hot-mounted — restart required";
536
- const HOST_CANNOT_HOT_MOUNT_REASON = "当前环境不支持热挂载,重启后生效 / this harness cannot hot-mount — restart required";
537
- const TIMEOUT_REASON = "热挂载超时,重启后生效 / hot-mount timed out — restart required";
538
- const MOUNT_FAILED_REASON = "热挂载失败,重启后生效 / hot-mount failed — restart required";
539
- const ID_LINE_RE = /^- id: (.+)$/;
540
- const NAME_LINE_RE = /^ name: (.+)$/;
541
544
  /**
542
545
  * Parse a bundle patch into the plain insert rows a hot tree can replicate.
543
- * Only `- id:` / `name:` pairs parse — a row with config, an expression
544
- * name, or a dangling id returns null, and the caller falls back to restart
545
- * activation. CRLF-aware: a patch authored with Windows line endings must
546
- * not read as "contains config rows" (the Windows-patch regression their
547
- * hot.ts documents). Pure: string in, rows out.
546
+ *
547
+ * Only an `insert` list of plain `id`/`name` entries parses. A bare id-keyed
548
+ * row is a TARGETING patch the loader looks the id up in the already
549
+ * composed tree and skips it when absent (applyEntryPatches), so it creates
550
+ * nothing and there is nothing to replicate. A row carrying config, a group,
551
+ * or an `!!js` expression name returns null too, and every null sends the
552
+ * caller to restart activation. Parsed as YAML in the loader's own dialect,
553
+ * so line endings and comments are the parser's problem, not ours, and
554
+ * nothing in the patch is evaluated. Pure: string in, rows out.
548
555
  */
549
556
  function parseSimplePatch(patchText) {
550
- const lines = patchText.split(/\r?\n/);
557
+ let parsed;
558
+ try {
559
+ parsed = load(patchText, { schema: PATCH_SCHEMA });
560
+ } catch {
561
+ return null;
562
+ }
563
+ if (!Array.isArray(parsed)) return null;
551
564
  const rows = [];
552
- for (let i = 0; i < lines.length; i++) {
553
- const raw = lines[i] ?? "";
554
- if (raw.trim() === "" || raw.trim().startsWith("#")) continue;
555
- const idMatch = ID_LINE_RE.exec(raw);
556
- if (idMatch === null) return null;
557
- const id = idMatch[1];
558
- if (id === void 0) return null;
559
- let name;
560
- while (i + 1 < lines.length) {
561
- i++;
562
- const next = lines[i] ?? "";
563
- if (next === "" || next.trim().startsWith("#")) continue;
564
- const nameMatch = NAME_LINE_RE.exec(next);
565
- if (nameMatch === null) return null;
566
- const value = nameMatch[1];
567
- if (value === void 0) return null;
568
- if (value.includes("!!js/expression")) return null;
569
- name = value;
570
- break;
565
+ for (const patch of parsed) {
566
+ if (patch === null || typeof patch !== "object" || Array.isArray(patch)) return null;
567
+ const keys = Object.keys(patch);
568
+ if (keys.length !== 1 || keys[0] !== "insert") return null;
569
+ const inserted = patch.insert;
570
+ if (!Array.isArray(inserted)) return null;
571
+ for (const item of inserted) {
572
+ if (item === null || typeof item !== "object" || Array.isArray(item)) return null;
573
+ const { id, name } = item;
574
+ if (typeof id !== "string" || typeof name !== "string") return null;
575
+ if (Object.keys(item).some((key) => key !== "id" && key !== "name")) return null;
576
+ rows.push({
577
+ id,
578
+ name
579
+ });
571
580
  }
572
- if (name === void 0) return null;
573
- rows.push({
574
- id,
575
- name
576
- });
577
581
  }
578
582
  return rows.length > 0 ? rows : null;
579
583
  }
@@ -637,7 +641,7 @@ function suppressWrite(treeClass) {
637
641
  * Include subtree: read its bundle patch, replicate the simple rows under
638
642
  * `mkt-` ids into `<profile>/.dsh-shop/hot-<n>.yml`, register the tree, and
639
643
  * race its activation against `timeoutMs`. Success returns `ok: true` with
640
- * no reason; any fallback returns `ok: false` with a bilingual reason
644
+ * no reason; any fallback returns `ok: false` with a reason code
641
645
  * distinguishing "restart will fix it" (timeout, activation failure,
642
646
  * unavailable include) from "this package can never hot-mount" (no patch
643
647
  * file, or rows the hot tree cannot replicate). The plugin is installed in
@@ -654,7 +658,7 @@ async function hotMount(ctx, profileDir, packageName, deps = {}) {
654
658
  ctx.logger?.warn(`hot-mount ${packageName}: no patch file to mount — restart will activate it`);
655
659
  return {
656
660
  ok: false,
657
- reason: NO_PATCH_REASON
661
+ reason: "no-patch"
658
662
  };
659
663
  }
660
664
  const rows = parseSimplePatch(patchText);
@@ -662,7 +666,7 @@ async function hotMount(ctx, profileDir, packageName, deps = {}) {
662
666
  ctx.logger?.warn(`hot-mount ${packageName}: patch has rows that cannot be hot-mounted — restart will activate it`);
663
667
  return {
664
668
  ok: false,
665
- reason: NOT_SIMPLE_REASON
669
+ reason: "not-simple"
666
670
  };
667
671
  }
668
672
  let treeClass = deps.hotTreeClass;
@@ -671,7 +675,7 @@ async function hotMount(ctx, profileDir, packageName, deps = {}) {
671
675
  ctx.logger?.warn(`hot-mount ${packageName}: the include plugin is unavailable in this harness — restart will activate it`);
672
676
  return {
673
677
  ok: false,
674
- reason: HOST_CANNOT_HOT_MOUNT_REASON
678
+ reason: "host-unsupported"
675
679
  };
676
680
  }
677
681
  const file = join(dir, `hot-${nextHotNumber(fs, dir)}.yml`);
@@ -684,7 +688,7 @@ async function hotMount(ctx, profileDir, packageName, deps = {}) {
684
688
  ctx.logger?.warn(`hot-mount ${packageName}: mounting the include tree failed — restart will activate it (${String(error)})`);
685
689
  return {
686
690
  ok: false,
687
- reason: MOUNT_FAILED_REASON
691
+ reason: "mount-failed"
688
692
  };
689
693
  }
690
694
  const previous = hotHandles.get(packageName);
@@ -712,7 +716,7 @@ async function hotMount(ctx, profileDir, packageName, deps = {}) {
712
716
  }
713
717
  return {
714
718
  ok: false,
715
- reason: outcome === "timeout" ? TIMEOUT_REASON : MOUNT_FAILED_REASON
719
+ reason: outcome === "timeout" ? "timeout" : "mount-failed"
716
720
  };
717
721
  }
718
722
  /** Race the fiber's activation against the deadline. The remaining time is
@@ -902,22 +906,99 @@ function realpathNearestExisting(startPath) {
902
906
  }
903
907
  }
904
908
  /**
905
- * Upsert one row of the profile's user layer (`cordis.patch.yml`). Enabling
906
- * removes the row so the bundle default rules again; disabling writes
907
- * `{ id, disabled: true }` (§8: the CLI's watchUserPatches applies the change
908
- * hot through HMR). Existing rows for other ids are preserved verbatim.
909
+ * Write every user-layer row of one package in a single pass.
910
+ *
911
+ * A package owns as many loader entries as its bundle patch inserts (see
912
+ * {@link ownedEntryIds}), and they toggle together: writing them one at a
913
+ * time would read-modify-write the file once per entry, so a crash between
914
+ * two writes would leave the package half disabled.
909
915
  */
910
- function setUserLayerRow(options) {
916
+ function setUserLayerRows(options) {
911
917
  const file = join(options.profileDir, "cordis.patch.yml");
912
- const others = (loadOptionalPatches("dsh-plugin-shop", file) ?? []).filter((row) => row.id !== options.row.id);
913
- const next = options.row.disabled ? [...others, {
914
- id: options.row.id,
918
+ const existing = loadOptionalPatches("dsh-plugin-shop", file) ?? [];
919
+ const touched = new Set(options.rows.map((row) => row.id));
920
+ const next = [...existing.filter((row) => !touched.has(row.id)), ...options.rows.filter((row) => row.disabled).map((row) => ({
921
+ id: row.id,
915
922
  disabled: true
916
- }] : others;
923
+ }))];
917
924
  const tmp = `${file}.tmp`;
918
925
  writeFileSync(tmp, dump(next, { noRefs: true }));
919
926
  renameSync(tmp, file);
920
927
  }
928
+ /**
929
+ * The loader entry ids one installed package contributes to the tree.
930
+ *
931
+ * This — not the entry's module name — is what identifies a package's rows.
932
+ * A package declares a bundle patch whose `insert` list may add zero, one, or
933
+ * many entries, and an inserted entry's `name` is the MODULE it mounts, which
934
+ * need not be the package at all: `@tt-a1i/archify-dsh` inserts a configured
935
+ * instance of `@deepseek-ai/dsh-skill-filesystem` and registers no module of
936
+ * its own. Matching a package to its rows by module name therefore finds
937
+ * nothing for such a package, which is why the toggle reported it as not
938
+ * installed. The ids the package's own patch inserts are the only honest
939
+ * answer, and the inventory's `entryId` is exactly that id verbatim.
940
+ *
941
+ * Ids the patch merely TARGETS (a bare id-keyed row overriding config) are
942
+ * not owned: those rows belong to whoever inserted them, and claiming them
943
+ * would let one package disable another's entries.
944
+ *
945
+ * A package with no bundle patch, or one absent from the profile, owns
946
+ * nothing and yields `[]` — a distinct fact from "the entry is not live",
947
+ * which only the inventory can answer.
948
+ */
949
+ function ownedEntryIds(options) {
950
+ const packageDir = join(options.profileDir, "node_modules", ...options.packageName.split("/"));
951
+ const manifestPath = join(packageDir, "package.json");
952
+ if (!existsSync(manifestPath)) return [];
953
+ let patchRelative;
954
+ try {
955
+ patchRelative = JSON.parse(readFileSync(manifestPath, "utf8")).dsh?.bundle?.patch;
956
+ } catch (error) {
957
+ throw new Error(`dsh-plugin-shop: failed to read ${manifestPath}: ${String(error)}`);
958
+ }
959
+ if (typeof patchRelative !== "string") return [];
960
+ const patchFile = resolve(packageDir, patchRelative);
961
+ const inside = relative(packageDir, patchFile);
962
+ if (inside === "" || inside.startsWith("..") || isAbsolute(inside)) throw new Error(`dsh-plugin-shop: ${options.packageName} declares a bundle patch outside its own directory: ${patchRelative}`);
963
+ const ids = [];
964
+ collectInsertedIds(loadOptionalPatches("dsh-plugin-shop", patchFile) ?? [], ids);
965
+ return [...new Set(ids)];
966
+ }
967
+ /** Walk a patch list, appending the id of every INSERTED entry. A patch row
968
+ * without `insert` targets an entry someone else composed — the loader's
969
+ * applyEntryPatches looks it up and skips it when absent, so it creates
970
+ * nothing and owns nothing. An inserted GROUP owns its children, which the
971
+ * loader reads from the group's own `config` array. */
972
+ function collectInsertedIds(rows, into) {
973
+ for (const row of rows) {
974
+ if (row === null || typeof row !== "object") continue;
975
+ const inserted = row.insert;
976
+ if (Array.isArray(inserted)) collectEntryIds(inserted, into);
977
+ }
978
+ }
979
+ function collectEntryIds(entries, into) {
980
+ for (const entry of entries) {
981
+ if (entry === null || typeof entry !== "object") continue;
982
+ const { id, group, config } = entry;
983
+ if (typeof id === "string") into.push(id);
984
+ if (group === true && Array.isArray(config)) collectEntryIds(config, into);
985
+ }
986
+ }
987
+ /**
988
+ * Does a live loader entry id belong to the package that owns `owned`?
989
+ *
990
+ * A boot-layer entry carries the bare id its bundle patch inserted. The
991
+ * shop's own hot subtree mounts that same row as `mkt-<id>` inside an Include
992
+ * tree, which the loader spells `include:<tree>:mkt-<id>` — the same plugin,
993
+ * one restart earlier. Both spellings are the package's own row.
994
+ */
995
+ function ownsEntryId(owned, entryId) {
996
+ if (owned.has(entryId)) return true;
997
+ const colon = entryId.lastIndexOf(":");
998
+ if (colon === -1) return false;
999
+ const tail = entryId.slice(colon + 1);
1000
+ return tail.startsWith("mkt-") && owned.has(tail.slice(4));
1001
+ }
921
1002
  //#endregion
922
1003
  //#region src/host/index.ts
923
1004
  /** ShopGateway: the Host half of dsh-plugin-shop (§5.1). */
@@ -1261,10 +1342,26 @@ let ShopGateway = (() => {
1261
1342
  * plain re-update no-ops on the empty diff (dsh-market themes.ts:74-93).
1262
1343
  * For an update swap this sequencing is mandatory, not defensive: two live
1263
1344
  * instances of a service-providing plugin would collide at provision. */
1264
- async liveDisable(name) {
1345
+ /** The package's owned entry ids, or none when its bundle patch cannot be
1346
+ * read. For the paths where a live disable is an optimization and the
1347
+ * operation must succeed regardless; `setEnabled` reports the failure
1348
+ * instead, because there the patch IS the answer being asked for. */
1349
+ ownedEntryIdsOrNone(packageName) {
1350
+ try {
1351
+ return ownedEntryIds({
1352
+ profileDir: this.profileDirResolved(),
1353
+ packageName
1354
+ });
1355
+ } catch {
1356
+ return [];
1357
+ }
1358
+ }
1359
+ async liveDisableIds(ids) {
1360
+ if (ids.length === 0) return false;
1361
+ const owned = new Set(ids);
1265
1362
  let found = false;
1266
1363
  for (const entry of this.loaderEntries()) {
1267
- if (entry.options.name !== name) continue;
1364
+ if (entry.id === void 0 || !ownsEntryId(owned, entry.id)) continue;
1268
1365
  for (let attempt = 0; attempt < 3; attempt++) {
1269
1366
  try {
1270
1367
  await entry.update({ disabled: true }, false, true);
@@ -1288,17 +1385,39 @@ let ShopGateway = (() => {
1288
1385
  ok: false,
1289
1386
  detail: `dsh-plugin-shop: ${args.name} is part of the harness chain and cannot be toggled from the shop`
1290
1387
  };
1291
- const entry = (await this.listInventory()).find((entry) => entry.moduleName === args.name);
1292
- if (entry === void 0) return {
1388
+ const profileDir = this.profileDirResolved();
1389
+ if ((readProfileManifest("dsh-plugin-shop", profileDir).dependencies ?? {})[args.name] === void 0) return {
1293
1390
  ok: false,
1294
1391
  detail: `dsh-plugin-shop: ${args.name} is not installed`
1295
1392
  };
1296
- setUserLayerRow({
1297
- profileDir: this.profileDirResolved(),
1298
- row: {
1299
- id: entry.entryId,
1393
+ let owned;
1394
+ try {
1395
+ owned = ownedEntryIds({
1396
+ profileDir,
1397
+ packageName: args.name
1398
+ });
1399
+ } catch (error) {
1400
+ return {
1401
+ ok: false,
1402
+ detail: `dsh-plugin-shop: ${args.name} has a bundle patch that could not be read: ${String(error)}`
1403
+ };
1404
+ }
1405
+ if (owned.length === 0) return {
1406
+ ok: false,
1407
+ detail: `dsh-plugin-shop: ${args.name} contributes no plugin entries, so there is nothing to enable or disable`
1408
+ };
1409
+ const ownedSet = new Set(owned);
1410
+ const rows = (await this.listInventory()).filter((entry) => ownsEntryId(ownedSet, entry.entryId)).map((entry) => entry.entryId);
1411
+ if (rows.length === 0) return {
1412
+ ok: false,
1413
+ detail: `dsh-plugin-shop: ${args.name} is installed but its entries are not in the running plugin tree; restart dsh to compose them`
1414
+ };
1415
+ setUserLayerRows({
1416
+ profileDir,
1417
+ rows: rows.map((id) => ({
1418
+ id,
1300
1419
  disabled: !args.enabled
1301
- }
1420
+ }))
1302
1421
  });
1303
1422
  return { ok: true };
1304
1423
  }
@@ -1390,6 +1509,7 @@ let ShopGateway = (() => {
1390
1509
  spec = `github:${entry.repo}#${args.version}${entry.subdir !== void 0 ? `&path:${entry.subdir}` : ""}`;
1391
1510
  } else spec = `${args.name}@${args.version}`;
1392
1511
  const isUpdate = (readProfileManifest("dsh-plugin-shop", this.profileDirResolved()).dependencies ?? {})[args.name] !== void 0;
1512
+ const priorEntryIds = isUpdate ? this.ownedEntryIdsOrNone(args.name) : [];
1393
1513
  const running = startInstall({
1394
1514
  profile: this.profile,
1395
1515
  spec,
@@ -1400,7 +1520,7 @@ let ShopGateway = (() => {
1400
1520
  mount: hotMount,
1401
1521
  unmount: hotUnmount
1402
1522
  };
1403
- if (isUpdate) await this.liveDisable(args.name);
1523
+ if (isUpdate) await this.liveDisableIds(priorEntryIds);
1404
1524
  const result = await hot.mount({ plugin: (plugin, config) => this.ctx.plugin(plugin, config) }, this.profileDirResolved(), args.name);
1405
1525
  return result.ok ? { needsRestart: false } : {
1406
1526
  needsRestart: true,
@@ -1464,10 +1584,30 @@ let ShopGateway = (() => {
1464
1584
  }
1465
1585
  const dependencies = readProfileManifest("dsh-plugin-shop", this.profileDirResolved()).dependencies ?? {};
1466
1586
  const pins = readRepoPins(this.pinFs, this.pinsPath());
1467
- let byName = /* @__PURE__ */ new Map();
1587
+ const live = /* @__PURE__ */ new Map();
1588
+ let haveInventory = false;
1468
1589
  try {
1469
- byName = new Map((await this.listInventory()).map((entry) => [entry.moduleName, entry.enabled]));
1590
+ for (const entry of await this.listInventory()) live.set(entry.entryId, entry.enabled);
1591
+ haveInventory = true;
1470
1592
  } catch {}
1593
+ /** A package is enabled when every entry it owns and that is live is
1594
+ * enabled. Keyed by entry id, never by module name — the entry a package
1595
+ * inserts may mount a different package's module (see ownedEntryIds). */
1596
+ const enabledOf = (name) => {
1597
+ if (!haveInventory) return true;
1598
+ let owned;
1599
+ try {
1600
+ owned = ownedEntryIds({
1601
+ profileDir: this.profileDirResolved(),
1602
+ packageName: name
1603
+ });
1604
+ } catch {
1605
+ return true;
1606
+ }
1607
+ const ownedSet = new Set(owned);
1608
+ const present = [...live].filter(([entryId]) => ownsEntryId(ownedSet, entryId));
1609
+ return present.length === 0 || present.every(([, enabled]) => enabled);
1610
+ };
1471
1611
  const installed = [];
1472
1612
  for (const entry of this.lastSnapshot.entries) {
1473
1613
  const spec = dependencies[entry.name];
@@ -1479,14 +1619,14 @@ let ShopGateway = (() => {
1479
1619
  installed: pin ?? spec,
1480
1620
  latest: entry.version,
1481
1621
  outdated: pin !== void 0 && pin !== entry.version,
1482
- enabled: byName.get(entry.name) ?? true
1622
+ enabled: enabledOf(entry.name)
1483
1623
  });
1484
1624
  } else installed.push({
1485
1625
  name: entry.name,
1486
1626
  installed: spec,
1487
1627
  latest: entry.version,
1488
1628
  outdated: this.isBehind(spec, entry.version),
1489
- enabled: byName.get(entry.name) ?? true
1629
+ enabled: enabledOf(entry.name)
1490
1630
  });
1491
1631
  }
1492
1632
  return installed;
@@ -1528,6 +1668,7 @@ let ShopGateway = (() => {
1528
1668
  ok: false,
1529
1669
  detail: `dsh-plugin-shop: ${args.name} is not installed`
1530
1670
  };
1671
+ const priorEntryIds = this.ownedEntryIdsOrNone(args.name);
1531
1672
  const running = startUninstall({
1532
1673
  profile: this.profile,
1533
1674
  name: args.name,
@@ -1537,7 +1678,7 @@ let ShopGateway = (() => {
1537
1678
  await (this.hot ?? {
1538
1679
  mount: hotMount,
1539
1680
  unmount: hotUnmount
1540
- }).unmount(args.name) || await this.liveDisable(args.name);
1681
+ }).unmount(args.name) || await this.liveDisableIds(priorEntryIds);
1541
1682
  return { needsRestart: false };
1542
1683
  }
1543
1684
  });
@@ -77,7 +77,7 @@ const dsh_plugin_shop_shop_installStatus_result$schema = z.object({
77
77
  'state': z.union([z.literal("running"), z.literal("done"), z.literal("failed")]),
78
78
  'log': z.array(z.string()),
79
79
  'needsRestart': z.union([z.undefined(), z.literal(false), z.literal(true)]).optional(),
80
- 'restartReason': z.union([z.undefined(), z.string()]).optional(),
80
+ 'restartReason': z.union([z.undefined(), z.literal("no-patch"), z.literal("not-simple"), z.literal("host-unsupported"), z.literal("timeout"), z.literal("mount-failed")]).optional(),
81
81
  'detail': z.union([z.undefined(), z.string()]).optional(),
82
82
  })
83
83
  const dsh_plugin_shop_shop_restart_result$schema = z.union([z.object({
@@ -151,7 +151,7 @@ export const TYPERT = {
151
151
  typeSymbol: 'dsh-plugin-shop/types#ShopCatalogResult',
152
152
  schema: dsh_plugin_shop_shop_catalog_result$schema,
153
153
  },
154
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":468,"column":9},
154
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":521,"column":9},
155
155
  },
156
156
  {
157
157
  id: 'dsh-plugin-shop#shop/installed',
@@ -166,7 +166,7 @@ export const TYPERT = {
166
166
  typeSymbol: 'dsh-plugin-shop#shop/installed:result',
167
167
  schema: dsh_plugin_shop_shop_installed_result$schema,
168
168
  },
169
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":618,"column":9},
169
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":677,"column":9},
170
170
  },
171
171
  {
172
172
  id: 'dsh-plugin-shop#shop/installStart',
@@ -192,7 +192,7 @@ export const TYPERT = {
192
192
  typeSymbol: 'dsh-plugin-shop/types#ShopInstallResult',
193
193
  schema: dsh_plugin_shop_shop_installStart_result$schema,
194
194
  },
195
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":494,"column":9},
195
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":547,"column":9},
196
196
  },
197
197
  {
198
198
  id: 'dsh-plugin-shop#shop/installStatus',
@@ -217,7 +217,7 @@ export const TYPERT = {
217
217
  typeSymbol: 'dsh-plugin-shop/types#ShopInstallStatusResult',
218
218
  schema: dsh_plugin_shop_shop_installStatus_result$schema,
219
219
  },
220
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":607,"column":3},
220
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":666,"column":3},
221
221
  },
222
222
  {
223
223
  id: 'dsh-plugin-shop#shop/restart',
@@ -232,7 +232,7 @@ export const TYPERT = {
232
232
  typeSymbol: 'dsh-plugin-shop/types#ShopRestartResult',
233
233
  schema: dsh_plugin_shop_shop_restart_result$schema,
234
234
  },
235
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":742,"column":9},
235
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":827,"column":9},
236
236
  },
237
237
  {
238
238
  id: 'dsh-plugin-shop#shop/setEnabled',
@@ -257,7 +257,7 @@ export const TYPERT = {
257
257
  typeSymbol: 'dsh-plugin-shop/types#ShopSetEnabledResult',
258
258
  schema: dsh_plugin_shop_shop_setEnabled_result$schema,
259
259
  },
260
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":421,"column":9},
260
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":442,"column":9},
261
261
  },
262
262
  {
263
263
  id: 'dsh-plugin-shop#shop/uninstallStart',
@@ -283,7 +283,7 @@ export const TYPERT = {
283
283
  typeSymbol: 'dsh-plugin-shop/types#ShopUninstallResult',
284
284
  schema: dsh_plugin_shop_shop_uninstallStart_result$schema,
285
285
  },
286
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":689,"column":9},
286
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":769,"column":9},
287
287
  },
288
288
  {
289
289
  id: 'dsh-plugin-shop#shop/updateStart',
@@ -308,7 +308,7 @@ export const TYPERT = {
308
308
  typeSymbol: 'dsh-plugin-shop/types#ShopUpdateResult',
309
309
  schema: dsh_plugin_shop_shop_updateStart_result$schema,
310
310
  },
311
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":802,"column":9},
311
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":887,"column":9},
312
312
  },
313
313
  {
314
314
  id: 'dsh-plugin-shop#shop/version',
@@ -323,7 +323,7 @@ export const TYPERT = {
323
323
  typeSymbol: 'dsh-plugin-shop/types#ShopVersionResult',
324
324
  schema: dsh_plugin_shop_shop_version_result$schema,
325
325
  },
326
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":786,"column":9},
326
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":871,"column":9},
327
327
  },
328
328
  ],
329
329
  model: {
@@ -423,6 +423,10 @@ export const TYPERT = {
423
423
  "name": "DeniedEntry",
424
424
  "declaration": "export interface DeniedEntry {\n name: string;\n detail: string;\n replacement?: string;\n}"
425
425
  },
426
+ {
427
+ "name": "HotRestartReason",
428
+ "declaration": "export type HotRestartReason = 'no-patch' | 'not-simple' | 'host-unsupported' | 'timeout' | 'mount-failed';"
429
+ },
426
430
  {
427
431
  "name": "InstallArgs",
428
432
  "declaration": "export interface InstallArgs {\n name: string;\n version: string;\n acknowledged?: boolean;\n}"
@@ -437,7 +441,7 @@ export const TYPERT = {
437
441
  },
438
442
  {
439
443
  "name": "InstallStatus",
440
- "declaration": "export interface InstallStatus {\n state: InstallState;\n log: string[];\n needsRestart?: boolean;\n restartReason?: string;\n detail?: string;\n}"
444
+ "declaration": "export interface InstallStatus {\n state: InstallState;\n log: string[];\n needsRestart?: boolean;\n restartReason?: HotRestartReason;\n detail?: string;\n}"
441
445
  },
442
446
  {
443
447
  "name": "RestartOutcome",
@@ -77,7 +77,7 @@ const dsh_plugin_shop_shop_installStatus_result$schema = z.object({
77
77
  'state': z.union([z.literal("running"), z.literal("done"), z.literal("failed")]),
78
78
  'log': z.array(z.string()),
79
79
  'needsRestart': z.union([z.undefined(), z.literal(false), z.literal(true)]).optional(),
80
- 'restartReason': z.union([z.undefined(), z.string()]).optional(),
80
+ 'restartReason': z.union([z.undefined(), z.literal("no-patch"), z.literal("not-simple"), z.literal("host-unsupported"), z.literal("timeout"), z.literal("mount-failed")]).optional(),
81
81
  'detail': z.union([z.undefined(), z.string()]).optional(),
82
82
  })
83
83
  const dsh_plugin_shop_shop_restart_result$schema = z.union([z.object({
@@ -148,7 +148,7 @@ export const TYPERT_REMOTE = {
148
148
  typeSymbol: 'dsh-plugin-shop/types#ShopCatalogResult',
149
149
  schema: dsh_plugin_shop_shop_catalog_result$schema,
150
150
  },
151
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":468,"column":9},
151
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":521,"column":9},
152
152
  },
153
153
  {
154
154
  id: 'dsh-plugin-shop#shop/installed',
@@ -163,7 +163,7 @@ export const TYPERT_REMOTE = {
163
163
  typeSymbol: 'dsh-plugin-shop#shop/installed:result',
164
164
  schema: dsh_plugin_shop_shop_installed_result$schema,
165
165
  },
166
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":618,"column":9},
166
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":677,"column":9},
167
167
  },
168
168
  {
169
169
  id: 'dsh-plugin-shop#shop/installStart',
@@ -189,7 +189,7 @@ export const TYPERT_REMOTE = {
189
189
  typeSymbol: 'dsh-plugin-shop/types#ShopInstallResult',
190
190
  schema: dsh_plugin_shop_shop_installStart_result$schema,
191
191
  },
192
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":494,"column":9},
192
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":547,"column":9},
193
193
  },
194
194
  {
195
195
  id: 'dsh-plugin-shop#shop/installStatus',
@@ -214,7 +214,7 @@ export const TYPERT_REMOTE = {
214
214
  typeSymbol: 'dsh-plugin-shop/types#ShopInstallStatusResult',
215
215
  schema: dsh_plugin_shop_shop_installStatus_result$schema,
216
216
  },
217
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":607,"column":3},
217
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":666,"column":3},
218
218
  },
219
219
  {
220
220
  id: 'dsh-plugin-shop#shop/restart',
@@ -229,7 +229,7 @@ export const TYPERT_REMOTE = {
229
229
  typeSymbol: 'dsh-plugin-shop/types#ShopRestartResult',
230
230
  schema: dsh_plugin_shop_shop_restart_result$schema,
231
231
  },
232
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":742,"column":9},
232
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":827,"column":9},
233
233
  },
234
234
  {
235
235
  id: 'dsh-plugin-shop#shop/setEnabled',
@@ -254,7 +254,7 @@ export const TYPERT_REMOTE = {
254
254
  typeSymbol: 'dsh-plugin-shop/types#ShopSetEnabledResult',
255
255
  schema: dsh_plugin_shop_shop_setEnabled_result$schema,
256
256
  },
257
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":421,"column":9},
257
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":442,"column":9},
258
258
  },
259
259
  {
260
260
  id: 'dsh-plugin-shop#shop/uninstallStart',
@@ -280,7 +280,7 @@ export const TYPERT_REMOTE = {
280
280
  typeSymbol: 'dsh-plugin-shop/types#ShopUninstallResult',
281
281
  schema: dsh_plugin_shop_shop_uninstallStart_result$schema,
282
282
  },
283
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":689,"column":9},
283
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":769,"column":9},
284
284
  },
285
285
  {
286
286
  id: 'dsh-plugin-shop#shop/updateStart',
@@ -305,7 +305,7 @@ export const TYPERT_REMOTE = {
305
305
  typeSymbol: 'dsh-plugin-shop/types#ShopUpdateResult',
306
306
  schema: dsh_plugin_shop_shop_updateStart_result$schema,
307
307
  },
308
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":802,"column":9},
308
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":887,"column":9},
309
309
  },
310
310
  {
311
311
  id: 'dsh-plugin-shop#shop/version',
@@ -320,7 +320,7 @@ export const TYPERT_REMOTE = {
320
320
  typeSymbol: 'dsh-plugin-shop/types#ShopVersionResult',
321
321
  schema: dsh_plugin_shop_shop_version_result$schema,
322
322
  },
323
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":786,"column":9},
323
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":871,"column":9},
324
324
  },
325
325
  ],
326
326
  }
@@ -30,6 +30,11 @@ export declare const zh: {
30
30
  acknowledgementBody: string;
31
31
  installing: string;
32
32
  installedRestartNotice: string;
33
+ hotNoPatchNotice: string;
34
+ hotNotSimpleNotice: string;
35
+ hotHostUnsupportedNotice: string;
36
+ hotTimeoutNotice: string;
37
+ hotMountFailedNotice: string;
33
38
  installedNoRestartNotice: string;
34
39
  installFailed: string;
35
40
  installTransportFailed: string;
@@ -109,6 +114,11 @@ export declare const en: {
109
114
  acknowledgementBody: string;
110
115
  installing: string;
111
116
  installedRestartNotice: string;
117
+ hotNoPatchNotice: string;
118
+ hotNotSimpleNotice: string;
119
+ hotHostUnsupportedNotice: string;
120
+ hotTimeoutNotice: string;
121
+ hotMountFailedNotice: string;
112
122
  installedNoRestartNotice: string;
113
123
  installFailed: string;
114
124
  installTransportFailed: string;
@@ -2,7 +2,11 @@
2
2
  * timers, no I/O: every function here maps a value to a value, so fixtures
3
3
  * drive all of it. */
4
4
  import type { ShopLocaleKey } from './locales.ts';
5
- import type { CatalogEntry, InstallRejectionCode } from '../host/index.ts';
5
+ import type { CatalogEntry, HotRestartReason, InstallRejectionCode } from '../host/index.ts';
6
+ /** Hot-mount reason code → locale key, so the notice reads in the language
7
+ * the person set in dsh. An absent (or unrecognized) reason keeps the generic
8
+ * restart line rather than showing a bare code. */
9
+ export declare function restartReasonKey(reason: HotRestartReason | undefined): ShopLocaleKey;
6
10
  /** Tier → locale key, for the entry-card tier badge (§6.2). */
7
11
  export declare function tierKey(tier: CatalogEntry['tier']): ShopLocaleKey;
8
12
  /** Spec §9.3 verbatim — the community-tier acknowledgement. The zh dictionary
@@ -37,7 +41,7 @@ export interface InstallStatusShape {
37
41
  state: 'running' | 'done' | 'failed';
38
42
  log: string[];
39
43
  needsRestart?: boolean;
40
- restartReason?: string;
44
+ restartReason?: HotRestartReason;
41
45
  detail?: string;
42
46
  }
43
47
  /** The install view state machine (§7.2). */
@@ -54,7 +58,7 @@ export type InstallView = {
54
58
  } | {
55
59
  kind: 'done';
56
60
  needsRestart: boolean;
57
- restartReason?: string;
61
+ restartReason?: HotRestartReason;
58
62
  } | {
59
63
  kind: 'failed';
60
64
  detail: string;
@@ -2,12 +2,13 @@
2
2
  * per profile. One implementation drives both `dsh plugin add` (install) and
3
3
  * `dsh plugin remove` (uninstall); the only differences are the verb and the
4
4
  * post-exit manifest confirmation. */
5
+ import type { HotRestartReason } from './hot.ts';
5
6
  export type InstallState = 'running' | 'done' | 'failed';
6
7
  export interface InstallStatus {
7
8
  state: InstallState;
8
9
  log: string[];
9
10
  needsRestart?: boolean;
10
- restartReason?: string;
11
+ restartReason?: HotRestartReason;
11
12
  detail?: string;
12
13
  }
13
14
  interface RunningInstall {
@@ -29,7 +30,7 @@ export declare function startInstall(options: {
29
30
  expectedName?: string;
30
31
  afterDone?: (home: string | undefined) => Promise<{
31
32
  needsRestart: boolean;
32
- restartReason?: string;
33
+ restartReason?: HotRestartReason;
33
34
  } | void>;
34
35
  onStatus?: (status: InstallStatus) => void;
35
36
  }): RunningInstall;
@@ -48,7 +49,7 @@ export declare function startUninstall(options: {
48
49
  expectedName?: string;
49
50
  afterDone?: (home: string | undefined) => Promise<{
50
51
  needsRestart: boolean;
51
- restartReason?: string;
52
+ restartReason?: HotRestartReason;
52
53
  } | void>;
53
54
  onStatus?: (status: InstallStatus) => void;
54
55
  }): RunningInstall;
@@ -16,12 +16,16 @@
16
16
  * tree changes back to the file it read (dsh-market hot.ts; the in-tree
17
17
  * precedent is dsh's agent-presets PresetTree).
18
18
  *
19
+ * Reasons are CODES, never copy: the host has no idea which language the
20
+ * person reading it set in dsh, so it names what happened and the client
21
+ * renders it through dsh's own locale service.
22
+ *
19
23
  * Deliberate non-port: dsh-market's client-only shim (`mountClientOnlyDeps`
20
24
  * and `shimNames`, which hot-mounted a package with no server-side entry by
21
25
  * inserting a shim loader entry) is omitted. Our catalog never lists a
22
26
  * package without `dsh.bundle`, so the shim branch is unreachable here
23
27
  * (YAGNI). `hotMount` still distinguishes "no patch file / not
24
- * hot-mountable" from "restart will fix it" through the bilingual `reason`.
28
+ * hot-mountable" from "restart will fix it" through the `reason` code.
25
29
  */
26
30
  /** One hot-mountable insert row: a plain `- id:` / `name:` pair. */
27
31
  export interface HotRow {
@@ -43,12 +47,17 @@ export interface PluginHandle {
43
47
  await(): Promise<unknown>;
44
48
  dispose(): Promise<unknown> | void;
45
49
  }
50
+ /**
51
+ * Why a hot mount could not activate — a stable code the client turns into
52
+ * copy in the reader's own dsh language. It distinguishes "restart will fix
53
+ * it" (`timeout`, `mount-failed`) from "this package can never hot-mount"
54
+ * (`no-patch`, `not-simple`) and "this harness cannot" (`host-unsupported`).
55
+ */
56
+ export type HotRestartReason = 'no-patch' | 'not-simple' | 'host-unsupported' | 'timeout' | 'mount-failed';
46
57
  export interface HotMountResult {
47
58
  ok: boolean;
48
- /** Bilingual, distinguishing "restart will fix it" (a transient mount
49
- * failure) from "this package can never hot-mount" (no patch file, or a
50
- * patch the hot tree cannot replicate). null exactly when ok. */
51
- reason: string | null;
59
+ /** null exactly when ok. */
60
+ reason: HotRestartReason | null;
52
61
  }
53
62
  /** Test injection for the host shell (filesystem + ctx + clock). Production
54
63
  * defaults: the real include import, node:fs, `join(profileDir, '.dsh-shop')`
@@ -71,11 +80,15 @@ export interface HotFs {
71
80
  }
72
81
  /**
73
82
  * Parse a bundle patch into the plain insert rows a hot tree can replicate.
74
- * Only `- id:` / `name:` pairs parse — a row with config, an expression
75
- * name, or a dangling id returns null, and the caller falls back to restart
76
- * activation. CRLF-aware: a patch authored with Windows line endings must
77
- * not read as "contains config rows" (the Windows-patch regression their
78
- * hot.ts documents). Pure: string in, rows out.
83
+ *
84
+ * Only an `insert` list of plain `id`/`name` entries parses. A bare id-keyed
85
+ * row is a TARGETING patch the loader looks the id up in the already
86
+ * composed tree and skips it when absent (applyEntryPatches), so it creates
87
+ * nothing and there is nothing to replicate. A row carrying config, a group,
88
+ * or an `!!js` expression name returns null too, and every null sends the
89
+ * caller to restart activation. Parsed as YAML in the loader's own dialect,
90
+ * so line endings and comments are the parser's problem, not ours, and
91
+ * nothing in the patch is evaluated. Pure: string in, rows out.
79
92
  */
80
93
  export declare function parseSimplePatch(patchText: string): HotRow[] | null;
81
94
  /**
@@ -83,7 +96,7 @@ export declare function parseSimplePatch(patchText: string): HotRow[] | null;
83
96
  * Include subtree: read its bundle patch, replicate the simple rows under
84
97
  * `mkt-` ids into `<profile>/.dsh-shop/hot-<n>.yml`, register the tree, and
85
98
  * race its activation against `timeoutMs`. Success returns `ok: true` with
86
- * no reason; any fallback returns `ok: false` with a bilingual reason
99
+ * no reason; any fallback returns `ok: false` with a reason code
87
100
  * distinguishing "restart will fix it" (timeout, activation failure,
88
101
  * unavailable include) from "this package can never hot-mount" (no patch
89
102
  * file, or rows the hot tree cannot replicate). The plugin is installed in
@@ -9,6 +9,7 @@ import { hotMount, hotUnmount } from './hot.ts';
9
9
  import { type RestartOutcome } from './restart.ts';
10
10
  import { type RepoPinFs } from './repo-pins.ts';
11
11
  export type { InstallArgs, InstallRejectionCode } from './install.ts';
12
+ export type { HotRestartReason } from './hot.ts';
12
13
  export type { CatalogEntry } from './types.ts';
13
14
  /** One Loader inventory entry, structurally — the shop never depends on
14
15
  * cordis-plugin-loader, whose types do not reach this package's typecheck. */
@@ -17,10 +18,11 @@ export interface InventoryEntry {
17
18
  moduleName: string;
18
19
  enabled: boolean;
19
20
  }
20
- /** One boot-layer Loader entry, structurally — the surface `liveDisable`
21
+ /** One boot-layer Loader entry, structurally — the surface `liveDisableIds`
21
22
  * consumes. `fiber` is the entry's live activation (present while the plugin
22
23
  * is up); `update` flips its options. */
23
24
  export interface LoaderEntryLike {
25
+ id?: string;
24
26
  options: {
25
27
  name?: string;
26
28
  };
@@ -258,7 +260,12 @@ export declare class ShopGateway extends TypertRemoteService {
258
260
  * plain re-update no-ops on the empty diff (dsh-market themes.ts:74-93).
259
261
  * For an update swap this sequencing is mandatory, not defensive: two live
260
262
  * instances of a service-providing plugin would collide at provision. */
261
- private liveDisable;
263
+ /** The package's owned entry ids, or none when its bundle patch cannot be
264
+ * read. For the paths where a live disable is an optimization and the
265
+ * operation must succeed regardless; `setEnabled` reports the failure
266
+ * instead, because there the patch IS the answer being asked for. */
267
+ private ownedEntryIdsOrNone;
268
+ private liveDisableIds;
262
269
  /** Enable or disable one installed plugin, hot (§8): a disable writes the
263
270
  * row to the user layer, an enable drops it again so the bundle default
264
271
  * rules — the CLI's watchUserPatches applies either through HMR. The shop's
@@ -28,3 +28,49 @@ export declare function setUserLayerRow(options: {
28
28
  profileDir: string;
29
29
  row: UserLayerRow;
30
30
  }): void;
31
+ /**
32
+ * Write every user-layer row of one package in a single pass.
33
+ *
34
+ * A package owns as many loader entries as its bundle patch inserts (see
35
+ * {@link ownedEntryIds}), and they toggle together: writing them one at a
36
+ * time would read-modify-write the file once per entry, so a crash between
37
+ * two writes would leave the package half disabled.
38
+ */
39
+ export declare function setUserLayerRows(options: {
40
+ profileDir: string;
41
+ rows: UserLayerRow[];
42
+ }): void;
43
+ /**
44
+ * The loader entry ids one installed package contributes to the tree.
45
+ *
46
+ * This — not the entry's module name — is what identifies a package's rows.
47
+ * A package declares a bundle patch whose `insert` list may add zero, one, or
48
+ * many entries, and an inserted entry's `name` is the MODULE it mounts, which
49
+ * need not be the package at all: `@tt-a1i/archify-dsh` inserts a configured
50
+ * instance of `@deepseek-ai/dsh-skill-filesystem` and registers no module of
51
+ * its own. Matching a package to its rows by module name therefore finds
52
+ * nothing for such a package, which is why the toggle reported it as not
53
+ * installed. The ids the package's own patch inserts are the only honest
54
+ * answer, and the inventory's `entryId` is exactly that id verbatim.
55
+ *
56
+ * Ids the patch merely TARGETS (a bare id-keyed row overriding config) are
57
+ * not owned: those rows belong to whoever inserted them, and claiming them
58
+ * would let one package disable another's entries.
59
+ *
60
+ * A package with no bundle patch, or one absent from the profile, owns
61
+ * nothing and yields `[]` — a distinct fact from "the entry is not live",
62
+ * which only the inventory can answer.
63
+ */
64
+ export declare function ownedEntryIds(options: {
65
+ profileDir: string;
66
+ packageName: string;
67
+ }): string[];
68
+ /**
69
+ * Does a live loader entry id belong to the package that owns `owned`?
70
+ *
71
+ * A boot-layer entry carries the bare id its bundle patch inserted. The
72
+ * shop's own hot subtree mounts that same row as `mkt-<id>` inside an Include
73
+ * tree, which the loader spells `include:<tree>:mkt-<id>` — the same plugin,
74
+ * one restart earlier. Both spellings are the package's own row.
75
+ */
76
+ export declare function ownsEntryId(owned: ReadonlySet<string>, entryId: string): boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-shop",
3
- "version": "0.5.2",
3
+ "version": "0.5.3-beta.0",
4
4
  "description": "The DeepSeek Harness plugin shop: browse, install, enable, and update dsh plugins from a git-auditable catalog.",
5
5
  "repository": {
6
6
  "type": "git",