dsh-plugin-shop 0.7.0 → 0.7.1-beta.1

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
@@ -4,12 +4,12 @@ import { loadOptionalPatches, readProfileManifest, resolveProfileDir } from "@de
4
4
  import { lt, minVersion, valid } from "semver";
5
5
  import { createHash, randomUUID } from "node:crypto";
6
6
  import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
7
- import { spawn, spawnSync } from "node:child_process";
8
7
  import { fileURLToPath, pathToFileURL } from "node:url";
9
- import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
8
+ import { basename, delimiter, dirname, isAbsolute, join, relative, resolve } from "node:path";
10
9
  import { homedir } from "node:os";
11
10
  import { z } from "zod";
12
11
  import { gunzipSync } from "node:zlib";
12
+ import { spawn } from "node:child_process";
13
13
  import { JSON_SCHEMA, Type, dump, load } from "js-yaml";
14
14
  //#region src/own-version.ts
15
15
  /** The shop's own published version, read from the package.json that ships
@@ -167,7 +167,7 @@ function verifyIntegrity(bytes, integrity, registryUrl) {
167
167
  const algorithm = dash === -1 ? "" : first.slice(0, dash);
168
168
  const expected = dash === -1 ? "" : first.slice(dash + 1);
169
169
  if (algorithm !== "sha512" && algorithm !== "sha256") throw new TransportError(`npm origin ${registryUrl}: unsupported dist.integrity algorithm ${JSON.stringify(algorithm)}`);
170
- if (createHash(algorithm).update(bytes).digest("base64") !== expected) throw new Error(`npm origin ${registryUrl}: tarball failed dist.integrity check (${algorithm})`);
170
+ if (createHash(algorithm).update(bytes).digest("base64") !== expected) throw new TransportError(`npm origin ${registryUrl}: tarball failed dist.integrity check (${algorithm})`);
171
171
  }
172
172
  /** Normalise to a trailing slash so relative `URL` resolution against a
173
173
  * registry that carries a path — every corporate registry, e.g.
@@ -440,7 +440,7 @@ const pointerSchema = z.object({
440
440
  sha256: z.string()
441
441
  }).optional()
442
442
  });
443
- const nodeFs$1 = {
443
+ const nodeFs$2 = {
444
444
  exists: (path) => existsSync(path),
445
445
  read: (path) => readFileSync(path, "utf8"),
446
446
  write: (path, data) => {
@@ -472,7 +472,7 @@ function parseStarsText(text) {
472
472
  * included, degrades to no stars (spec §5).
473
473
  */
474
474
  async function loadCatalog(options) {
475
- const { cacheDir, refresh = false, fetchImpl = fetch, now = () => /* @__PURE__ */ new Date(), fsImpl = nodeFs$1 } = options;
475
+ const { cacheDir, refresh = false, fetchImpl = fetch, now = () => /* @__PURE__ */ new Date(), fsImpl = nodeFs$2 } = options;
476
476
  if (options.baseUrl === void 0 === (options.origins === void 0)) throw new Error("loadCatalog: exactly one of baseUrl or origins is required");
477
477
  const originList = options.origins ?? [httpOrigin(options.baseUrl, fetchImpl)];
478
478
  if (originList.length === 0) throw new Error("loadCatalog: no origins");
@@ -695,6 +695,113 @@ function validateInstall(snapshot, args) {
695
695
  return { ok: true };
696
696
  }
697
697
  //#endregion
698
+ //#region src/host/dsh-cli.ts
699
+ /**
700
+ * How to start the dsh CLI as a child process.
701
+ *
702
+ * On POSIX this is `spawn('dsh', …)` and there is nothing to decide. On
703
+ * Windows there is no form of that call which can work: npm installs a CLI as
704
+ * `dsh`, `dsh.cmd` and `dsh.ps1` shims with no `.exe`, libuv resolves a bare
705
+ * name against `.com` and `.exe` only, and node has refused to spawn a `.cmd`
706
+ * without a shell since the 2024 batfile argument-injection fix. Measured on
707
+ * Windows 11 with dsh 0.1.1-rc.2 (2026-09-02): the bare name gives ENOENT,
708
+ * the resolved `.cmd` shim throws EINVAL *synchronously* out of `spawn()`.
709
+ *
710
+ * `shell: true` — what the shim exists for, and what dsh's own pnpm spawn
711
+ * uses — is not available to us. node hands cmd.exe a joined, UNQUOTED
712
+ * command string, and our argv carries catalog data: a `github:` entry's spec
713
+ * is `github:owner/slug#<sha>&path:<subdir>`, `&` is a cmd command separator,
714
+ * and `subdir` comes from npm. That is a command-injection surface on
715
+ * untrusted input, so the shop resolves the CLI's JS entry and runs it through
716
+ * node instead — no shell, and node escapes the argv itself.
717
+ *
718
+ * Pure core, impure shell: `dshCommand` decides, `resolveDshScript` takes the
719
+ * process snapshot and a filesystem seam as parameters (the same shape as
720
+ * `detectSupervisor(env, proc)` and `RepoPinFs`).
721
+ */
722
+ /** The npm package the dsh CLI is published as. */
723
+ const DSH_PACKAGE = "@deepseek-ai/dsh";
724
+ /** The bin name that package declares (`bin: { dsh: 'lib/bin.js' }`), which is
725
+ * also the unscoped name its plain-string `bin` form would take. */
726
+ const DSH_BIN_NAME = "dsh";
727
+ /**
728
+ * The command that starts the dsh CLI with `args`.
729
+ *
730
+ * The node route replaces exactly one thing: looking the bare name `dsh` up
731
+ * on PATH, which on Windows can never succeed. A `dshBin` naming a specific
732
+ * file is the caller's explicit choice and is spawned as given — honoring it
733
+ * and reporting the real failure beats silently running something else.
734
+ */
735
+ function dshCommand(options) {
736
+ const { dshBin, args, platform, execPath, script } = options;
737
+ if (platform === "win32" && script !== null && dshBin === DSH_BIN_NAME) return {
738
+ command: execPath,
739
+ args: [script, ...args]
740
+ };
741
+ return {
742
+ command: dshBin,
743
+ args: [...args]
744
+ };
745
+ }
746
+ /** The dsh entry a package manifest declares, or null when this manifest is
747
+ * not the dsh CLI. Everything read here is outside our control, so a manifest
748
+ * that will not parse is simply not a match. */
749
+ function declaredEntry(fs, manifestPath) {
750
+ let parsed;
751
+ try {
752
+ parsed = JSON.parse(fs.read(manifestPath));
753
+ } catch {
754
+ return null;
755
+ }
756
+ const { name, bin } = parsed;
757
+ if (name !== "@deepseek-ai/dsh") return null;
758
+ const declared = typeof bin === "string" ? bin : bin?.[DSH_BIN_NAME];
759
+ if (typeof declared !== "string") return null;
760
+ const entry = join(dirname(manifestPath), declared);
761
+ return fs.exists(entry) ? entry : null;
762
+ }
763
+ /** The dsh entry declared by the package that OWNS `script` — the nearest
764
+ * `package.json` at or above it, and only that one. A script whose owner is
765
+ * some other package is not dsh's, so the search stops rather than walking up
766
+ * into an unrelated workspace root. */
767
+ function owningEntry(fs, script) {
768
+ let dir = dirname(resolve(script));
769
+ for (;;) {
770
+ const manifestPath = join(dir, "package.json");
771
+ if (fs.exists(manifestPath)) return declaredEntry(fs, manifestPath);
772
+ const parent = dirname(dir);
773
+ if (parent === dir) return null;
774
+ dir = parent;
775
+ }
776
+ }
777
+ /**
778
+ * Locate the dsh CLI's JS entry, or null when it cannot be found.
779
+ *
780
+ * `argv1` first: the shop is loaded BY dsh, so the running script is dsh's own
781
+ * entry, and reusing it guarantees the child is the same dsh that is serving
782
+ * the shop rather than whichever one PATH happens to name. The owner check is
783
+ * what makes this safe to try — under a test runner `argv1` belongs to the
784
+ * runner, and accepting it would spawn the wrong program.
785
+ *
786
+ * Then the shims on PATH: npm installs the package into `node_modules` beside
787
+ * them, so the manifest is one join away. This is the branch a test harness
788
+ * takes, which is why the real-installation test exercises it.
789
+ */
790
+ function resolveDshScript(fs, options) {
791
+ if (options.argv1 !== void 0) {
792
+ const owned = owningEntry(fs, options.argv1);
793
+ if (owned !== null) return owned;
794
+ }
795
+ for (const dir of (options.path ?? "").split(delimiter)) {
796
+ if (dir === "") continue;
797
+ const manifestPath = join(dir, "node_modules", ...DSH_PACKAGE.split("/"), "package.json");
798
+ if (!fs.exists(manifestPath)) continue;
799
+ const entry = declaredEntry(fs, manifestPath);
800
+ if (entry !== null) return entry;
801
+ }
802
+ return null;
803
+ }
804
+ //#endregion
698
805
  //#region src/host/executor.ts
699
806
  /** Plugin-command executor: spawn the dsh CLI, stream its output, serialize
700
807
  * per profile. One implementation drives both `dsh plugin add` (install) and
@@ -739,6 +846,78 @@ function confirmBundleRemoval(profile, home, expectedName) {
739
846
  return `removed but the profile manifest could not be read (${join(profileDir, "package.json")}) — re-run the uninstall`;
740
847
  }
741
848
  }
849
+ /** Lines a failed install's log tends to END on, none of which tell the user
850
+ * anything they can act on: dsh's own wrapper around the pnpm exit, node's
851
+ * version footer, pnpm's progress and update banner, and the frames and
852
+ * carets of a stack trace. */
853
+ const FAILURE_LOG_NOISE = [
854
+ /^dsh: pnpm failed in profile directory/,
855
+ /^Node\.js v/,
856
+ /^(?:Progress|Packages):/,
857
+ /^\++$/,
858
+ /^[╭│╰]/,
859
+ /^\^+$/,
860
+ /^\s*$/,
861
+ /^\s+at /,
862
+ /^<anonymous(?:_script)?>/
863
+ ];
864
+ /**
865
+ * The one log line worth putting in front of the user, plus the recovery hint.
866
+ *
867
+ * The last line is the wrong choice, which is what this replaces. Measured on
868
+ * two real failures against the live catalog (dsh 0.1.1-rc.2, 2026-09-02),
869
+ * the last line was `dsh: pnpm failed in profile directory <a path the user
870
+ * did not choose>` and `Node.js v26.6.0` — while the line that explained the
871
+ * failure sat a few lines above, in the log the client already receives. A
872
+ * user seeing either of those cannot tell a blocked build script from a
873
+ * crash, which is exactly the report this fixes.
874
+ *
875
+ * Scanning from the end: a pnpm error code first, then any thrown error, then
876
+ * the last line that is not noise.
877
+ */
878
+ function installFailureDetail(profile, log) {
879
+ const hint = `pnpm failed in the profile. Run: dsh plugin --profile ${profile} install`;
880
+ const usable = log.filter((line) => !FAILURE_LOG_NOISE.some((rx) => rx.test(line)));
881
+ const reversed = [...usable].reverse();
882
+ const pick = reversed.find((line) => /ERR_[A-Z][A-Z_]*/.test(line)) ?? reversed.find((line) => /(?:^|\s)\w*Error:/.test(line)) ?? usable[usable.length - 1];
883
+ if (pick === void 0) return `${hint}.`;
884
+ return `${hint} — ${pick}${/ERR_PNPM_IGNORED_BUILDS/.test(pick) ? " A dependency wants to run a build script, which pnpm blocks by default: run `pnpm approve-builds` in the profile directory to allow it, then retry." : ""}`;
885
+ }
886
+ /**
887
+ * Why a spawn of the dsh CLI never started.
888
+ *
889
+ * On Windows a bare name and a shim path fail differently and neither means
890
+ * what the POSIX advice says. npm installs the CLI as `dsh`, `dsh.cmd` and
891
+ * `dsh.ps1` with no `.exe`; libuv resolves a bare name against `.com` and
892
+ * `.exe` only (ENOENT), and node has refused to spawn a `.cmd` without a
893
+ * shell since the 2024 batfile argument-injection fix (EINVAL). `dsh-cli.ts`
894
+ * gets past that by running the CLI's own JS entry through node, so reaching
895
+ * here on Windows means that entry could not be located — telling the user to
896
+ * install a dsh they already have would still be wrong. Reported from Windows
897
+ * 2026-09-02 as "Update failed / dsh not found on PATH" on a working install.
898
+ */
899
+ function spawnFailureDetail(code, message, dshBin, platform) {
900
+ 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.`;
901
+ if (code === "ENOENT") return "dsh not found on PATH — install the dsh CLI to manage profile plugins";
902
+ return `dsh spawn failed: ${message}`;
903
+ }
904
+ /** Read-only filesystem seam for the CLI lookup; the same shape as `pinFs`. */
905
+ const nodeFs$1 = {
906
+ exists: (path) => existsSync(path),
907
+ read: (path) => readFileSync(path, "utf8")
908
+ };
909
+ let cachedScript;
910
+ /** The dsh CLI's JS entry, resolved once per process and only where it is
911
+ * needed. On POSIX `spawn('dsh')` works and the lookup is skipped entirely,
912
+ * so no platform but Windows pays for it. */
913
+ function dshScript() {
914
+ if (process.platform !== "win32") return null;
915
+ if (cachedScript === void 0) cachedScript = resolveDshScript(nodeFs$1, {
916
+ argv1: process.argv[1],
917
+ path: process.env.PATH
918
+ });
919
+ return cachedScript;
920
+ }
742
921
  /** Run one `dsh plugin --profile <profile> <verb> <target>` and track it.
743
922
  * Never rolls back; a failure surfaces stderr verbatim plus the recovery hint
744
923
  * (§10). The shop never passes build-script flags: `allowBuilds` stays the
@@ -782,23 +961,42 @@ function spawnPluginCli(options) {
782
961
  }
783
962
  onStatus?.(status());
784
963
  };
964
+ const failToStart = (error) => {
965
+ state = "failed";
966
+ detail = spawnFailureDetail(error.code, error.message, dshBin, process.platform);
967
+ onStatus?.(status());
968
+ return status();
969
+ };
785
970
  return {
786
971
  installId,
787
972
  status,
788
973
  finished: chain(profile, () => new Promise((resolve) => {
789
- const child = spawn(dshBin, [
790
- "plugin",
791
- "--profile",
792
- profile,
793
- ...argv
794
- ], {
795
- stdio: [
796
- "ignore",
797
- "pipe",
798
- "pipe"
974
+ const { command, args } = dshCommand({
975
+ dshBin,
976
+ args: [
977
+ "plugin",
978
+ "--profile",
979
+ profile,
980
+ ...argv
799
981
  ],
800
- env: env ?? process.env
982
+ platform: process.platform,
983
+ execPath: process.execPath,
984
+ script: dshScript()
801
985
  });
986
+ let child;
987
+ try {
988
+ child = spawn(command, args, {
989
+ stdio: [
990
+ "ignore",
991
+ "pipe",
992
+ "pipe"
993
+ ],
994
+ env: env ?? process.env
995
+ });
996
+ } catch (error) {
997
+ resolve(failToStart(error));
998
+ return;
999
+ }
802
1000
  child.stdout.on("data", (chunk) => {
803
1001
  for (const line of chunk.toString().split("\n")) if (line !== "") append(line);
804
1002
  });
@@ -806,11 +1004,7 @@ function spawnPluginCli(options) {
806
1004
  for (const line of chunk.toString().split("\n")) if (line !== "") append(line);
807
1005
  });
808
1006
  child.on("error", (error) => {
809
- const code = error.code;
810
- state = "failed";
811
- detail = code === "ENOENT" ? "dsh not found on PATH — install the dsh CLI to manage profile plugins" : `dsh spawn failed: ${error.message}`;
812
- onStatus?.(status());
813
- resolve(status());
1007
+ resolve(failToStart(error));
814
1008
  });
815
1009
  child.on("close", async (exitCode) => {
816
1010
  if (state !== "running") return;
@@ -832,8 +1026,7 @@ function spawnPluginCli(options) {
832
1026
  } else state = "done";
833
1027
  } else {
834
1028
  state = "failed";
835
- const lastLogLine = log[log.length - 1] ?? "";
836
- detail = `pnpm failed in the profile. Run: dsh plugin --profile ${profile} install — ${lastLogLine}`;
1029
+ detail = installFailureDetail(profile, log);
837
1030
  }
838
1031
  onStatus?.(status());
839
1032
  resolve(status());
@@ -1174,7 +1367,13 @@ function cleanHotDir(profileDir) {
1174
1367
  *
1175
1368
  * The pid-poll has the usual tiny reuse race — if the parent's pid is
1176
1369
  * recycled within the 0.2s polling gap the helper waits for the unrelated
1177
- * process too. Harmless: it only delays the boot. */
1370
+ * process too. Harmless: it only delays the boot.
1371
+ *
1372
+ * POSIX only: `sh`, `kill -0` and `sleep` are all Unix, and `exec "$@"` has
1373
+ * no Windows equivalent. The spawn would fail ASYNCHRONOUSLY, after this
1374
+ * function has already returned and the caller has committed — so the gateway
1375
+ * refuses on Windows before reaching here rather than exiting into nothing
1376
+ * (index.ts `restartPlatformSupported`). */
1178
1377
  function startRestart(options) {
1179
1378
  const { dshBin, argv, parentPid, logFile, env } = options;
1180
1379
  const logFd = openSync(logFile, "a");
@@ -1704,10 +1903,12 @@ let ShopGateway = (() => {
1704
1903
  restartExitDelayMs;
1705
1904
  restartParentPid;
1706
1905
  latestVersion;
1707
- hasGit;
1708
1906
  pinFs;
1709
1907
  allowRestart;
1710
1908
  env;
1909
+ /** The platform the restart gate reads; production defaults to
1910
+ * process.platform. The two-phase handoff is POSIX-only (restart.ts). */
1911
+ platform;
1711
1912
  /** The parent pid `detectSupervisor` inspects; production defaults to
1712
1913
  * process.ppid (a systemd unit's main process has ppid 1). */
1713
1914
  ppid;
@@ -1756,7 +1957,6 @@ let ShopGateway = (() => {
1756
1957
  this.restartExitDelayMs = options.restartExitDelayMs ?? ShopGateway.RESTART_EXIT_DELAY_MS;
1757
1958
  this.restartParentPid = options.restartParentPid ?? process.pid;
1758
1959
  this.latestVersion = options.fetchLatestVersion ?? (() => fetchLatestVersion());
1759
- this.hasGit = options.hasGit ?? (() => spawnSync("git", ["--version"], { stdio: "ignore" }).status === 0);
1760
1960
  this.pinFs = options.pinFs ?? {
1761
1961
  exists: (path) => existsSync(path),
1762
1962
  read: (path) => readFileSync(path, "utf8"),
@@ -1767,6 +1967,7 @@ let ShopGateway = (() => {
1767
1967
  };
1768
1968
  this.allowRestart = options.allowRestart;
1769
1969
  this.env = options.env ?? process.env;
1970
+ this.platform = options.platform ?? process.platform;
1770
1971
  this.ppid = options.ppid ?? process.ppid;
1771
1972
  this.fetchTarball = options.fetchTarball ?? ((url) => fetch(url));
1772
1973
  try {
@@ -1936,6 +2137,11 @@ let ShopGateway = (() => {
1936
2137
  /** The explicit restart override. Only the row's `config:` sub-object is
1937
2138
  * passed to a plugin — a top-level `allowRestart:` beside `name:` would be
1938
2139
  * silently ignored by the loader (dsh-market README, #227). */
2140
+ /** Whether the two-phase handoff can run at all here. `restart.ts` drives
2141
+ * it through `sh`, `kill -0` and `sleep`, none of which Windows has. */
2142
+ restartPlatformSupported() {
2143
+ return this.platform !== "win32";
2144
+ }
1939
2145
  allowRestartConfigured() {
1940
2146
  if (this.allowRestart !== void 0) return this.allowRestart;
1941
2147
  return ((this.ctx.loader?.entries().find((entry) => entry.options.name === "dsh-plugin-shop"))?.options.config)?.allowRestart === true;
@@ -2014,11 +2220,6 @@ let ShopGateway = (() => {
2014
2220
  code: "version-mismatch",
2015
2221
  detail: `dsh-plugin-shop: ${args.name} has no installable commit`
2016
2222
  };
2017
- if (!this.hasGit()) return {
2018
- ok: false,
2019
- code: "git-missing",
2020
- detail: "dsh-plugin-shop: git is not on PATH, which github installs require; install git and retry"
2021
- };
2022
2223
  spec = `github:${entry.repo}#${args.version}${entry.subdir !== void 0 ? `&path:${entry.subdir}` : ""}`;
2023
2224
  } else spec = `${args.name}@${args.version}`;
2024
2225
  const isUpdate = (readProfileManifest("dsh-plugin-shop", this.profileDirResolved()).dependencies ?? {})[args.name] !== void 0;
@@ -2214,6 +2415,10 @@ let ShopGateway = (() => {
2214
2415
  * response is out. The browser monitors the origin and refreshes when the
2215
2416
  * new server answers. Refusals are issued before anything is torn down. */
2216
2417
  async restart() {
2418
+ if (!this.restartPlatformSupported()) return {
2419
+ ok: false,
2420
+ detail: "dsh-plugin-shop: restart is not supported on Windows yet; restart dsh manually to apply the change"
2421
+ };
2217
2422
  if (detectSupervisor(this.env, { ppid: this.ppid }) === "systemd" && !this.allowRestartConfigured()) return {
2218
2423
  ok: false,
2219
2424
  detail: "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."
@@ -2252,7 +2457,7 @@ let ShopGateway = (() => {
2252
2457
  installed,
2253
2458
  latest,
2254
2459
  outdated: latest !== null && lt(installed, latest),
2255
- restartSupported: detectSupervisor(this.env, { ppid: this.ppid }) === null || this.allowRestartConfigured()
2460
+ restartSupported: this.restartPlatformSupported() && (detectSupervisor(this.env, { ppid: this.ppid }) === null || this.allowRestartConfigured())
2256
2461
  };
2257
2462
  }
2258
2463
  /** Update the shop itself to a published version (§7.3): the explicit pin
@@ -69,7 +69,7 @@ const dsh_plugin_shop_shop_installStart_result$schema = z.union([z.object({
69
69
  'installId': z.string(),
70
70
  }), z.object({
71
71
  'ok': z.literal(false),
72
- 'code': z.union([z.literal("not-in-catalog"), z.literal("denied"), z.literal("version-mismatch"), z.literal("needs-acknowledgement"), z.literal("git-missing"), z.literal("tarball-integrity")]),
72
+ 'code': z.union([z.literal("not-in-catalog"), z.literal("denied"), z.literal("version-mismatch"), z.literal("needs-acknowledgement"), z.literal("tarball-integrity")]),
73
73
  'detail': z.string(),
74
74
  })])
75
75
  const dsh_plugin_shop_shop_installStatus_parameter_0$schema = z.object({
@@ -154,7 +154,7 @@ export const TYPERT = {
154
154
  typeSymbol: 'dsh-plugin-shop/types#ShopCatalogResult',
155
155
  schema: dsh_plugin_shop_shop_catalog_result$schema,
156
156
  },
157
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":568,"column":9},
157
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":574,"column":9},
158
158
  },
159
159
  {
160
160
  id: 'dsh-plugin-shop#shop/installed',
@@ -169,7 +169,7 @@ export const TYPERT = {
169
169
  typeSymbol: 'dsh-plugin-shop#shop/installed:result',
170
170
  schema: dsh_plugin_shop_shop_installed_result$schema,
171
171
  },
172
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":748,"column":9},
172
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":759,"column":9},
173
173
  },
174
174
  {
175
175
  id: 'dsh-plugin-shop#shop/installStart',
@@ -195,7 +195,7 @@ export const TYPERT = {
195
195
  typeSymbol: 'dsh-plugin-shop/types#ShopInstallResult',
196
196
  schema: dsh_plugin_shop_shop_installStart_result$schema,
197
197
  },
198
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":618,"column":9},
198
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":624,"column":9},
199
199
  },
200
200
  {
201
201
  id: 'dsh-plugin-shop#shop/installStatus',
@@ -220,7 +220,7 @@ export const TYPERT = {
220
220
  typeSymbol: 'dsh-plugin-shop/types#ShopInstallStatusResult',
221
221
  schema: dsh_plugin_shop_shop_installStatus_result$schema,
222
222
  },
223
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":737,"column":3},
223
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":748,"column":3},
224
224
  },
225
225
  {
226
226
  id: 'dsh-plugin-shop#shop/restart',
@@ -235,7 +235,7 @@ export const TYPERT = {
235
235
  typeSymbol: 'dsh-plugin-shop/types#ShopRestartResult',
236
236
  schema: dsh_plugin_shop_shop_restart_result$schema,
237
237
  },
238
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":898,"column":9},
238
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":909,"column":9},
239
239
  },
240
240
  {
241
241
  id: 'dsh-plugin-shop#shop/setEnabled',
@@ -286,7 +286,7 @@ export const TYPERT = {
286
286
  typeSymbol: 'dsh-plugin-shop/types#ShopUninstallResult',
287
287
  schema: dsh_plugin_shop_shop_uninstallStart_result$schema,
288
288
  },
289
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":840,"column":9},
289
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":851,"column":9},
290
290
  },
291
291
  {
292
292
  id: 'dsh-plugin-shop#shop/updateStart',
@@ -311,7 +311,7 @@ export const TYPERT = {
311
311
  typeSymbol: 'dsh-plugin-shop/types#ShopUpdateResult',
312
312
  schema: dsh_plugin_shop_shop_updateStart_result$schema,
313
313
  },
314
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":958,"column":9},
314
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":977,"column":9},
315
315
  },
316
316
  {
317
317
  id: 'dsh-plugin-shop#shop/version',
@@ -326,7 +326,7 @@ export const TYPERT = {
326
326
  typeSymbol: 'dsh-plugin-shop/types#ShopVersionResult',
327
327
  schema: dsh_plugin_shop_shop_version_result$schema,
328
328
  },
329
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":942,"column":9},
329
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":960,"column":9},
330
330
  },
331
331
  ],
332
332
  model: {
@@ -436,7 +436,7 @@ export const TYPERT = {
436
436
  },
437
437
  {
438
438
  "name": "InstallRejectionCode",
439
- "declaration": "export type InstallRejectionCode = 'not-in-catalog' | 'denied' | 'version-mismatch' | 'needs-acknowledgement' | 'git-missing' | 'tarball-integrity';"
439
+ "declaration": "export type InstallRejectionCode = 'not-in-catalog' | 'denied' | 'version-mismatch' | 'needs-acknowledgement' | 'tarball-integrity';"
440
440
  },
441
441
  {
442
442
  "name": "InstallState",
@@ -69,7 +69,7 @@ const dsh_plugin_shop_shop_installStart_result$schema = z.union([z.object({
69
69
  'installId': z.string(),
70
70
  }), z.object({
71
71
  'ok': z.literal(false),
72
- 'code': z.union([z.literal("not-in-catalog"), z.literal("denied"), z.literal("version-mismatch"), z.literal("needs-acknowledgement"), z.literal("git-missing"), z.literal("tarball-integrity")]),
72
+ 'code': z.union([z.literal("not-in-catalog"), z.literal("denied"), z.literal("version-mismatch"), z.literal("needs-acknowledgement"), z.literal("tarball-integrity")]),
73
73
  'detail': z.string(),
74
74
  })])
75
75
  const dsh_plugin_shop_shop_installStatus_parameter_0$schema = z.object({
@@ -151,7 +151,7 @@ export const TYPERT_REMOTE = {
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":568,"column":9},
154
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":574,"column":9},
155
155
  },
156
156
  {
157
157
  id: 'dsh-plugin-shop#shop/installed',
@@ -166,7 +166,7 @@ export const TYPERT_REMOTE = {
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":748,"column":9},
169
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":759,"column":9},
170
170
  },
171
171
  {
172
172
  id: 'dsh-plugin-shop#shop/installStart',
@@ -192,7 +192,7 @@ export const TYPERT_REMOTE = {
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":618,"column":9},
195
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":624,"column":9},
196
196
  },
197
197
  {
198
198
  id: 'dsh-plugin-shop#shop/installStatus',
@@ -217,7 +217,7 @@ export const TYPERT_REMOTE = {
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":737,"column":3},
220
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":748,"column":3},
221
221
  },
222
222
  {
223
223
  id: 'dsh-plugin-shop#shop/restart',
@@ -232,7 +232,7 @@ export const TYPERT_REMOTE = {
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":898,"column":9},
235
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":909,"column":9},
236
236
  },
237
237
  {
238
238
  id: 'dsh-plugin-shop#shop/setEnabled',
@@ -283,7 +283,7 @@ export const TYPERT_REMOTE = {
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":840,"column":9},
286
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":851,"column":9},
287
287
  },
288
288
  {
289
289
  id: 'dsh-plugin-shop#shop/updateStart',
@@ -308,7 +308,7 @@ export const TYPERT_REMOTE = {
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":958,"column":9},
311
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":977,"column":9},
312
312
  },
313
313
  {
314
314
  id: 'dsh-plugin-shop#shop/version',
@@ -323,7 +323,7 @@ export const TYPERT_REMOTE = {
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":942,"column":9},
326
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":960,"column":9},
327
327
  },
328
328
  ],
329
329
  }
@@ -56,7 +56,6 @@ export declare const zh: {
56
56
  notInCatalogCode: string;
57
57
  versionMismatchCode: string;
58
58
  needsAcknowledgementCode: string;
59
- gitMissingCode: string;
60
59
  tarballIntegrityCode: string;
61
60
  npmSource: string;
62
61
  githubSource: string;
@@ -147,7 +146,6 @@ export declare const en: {
147
146
  notInCatalogCode: string;
148
147
  versionMismatchCode: string;
149
148
  needsAcknowledgementCode: string;
150
- gitMissingCode: string;
151
149
  tarballIntegrityCode: string;
152
150
  npmSource: string;
153
151
  githubSource: string;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * How to start the dsh CLI as a child process.
3
+ *
4
+ * On POSIX this is `spawn('dsh', …)` and there is nothing to decide. On
5
+ * Windows there is no form of that call which can work: npm installs a CLI as
6
+ * `dsh`, `dsh.cmd` and `dsh.ps1` shims with no `.exe`, libuv resolves a bare
7
+ * name against `.com` and `.exe` only, and node has refused to spawn a `.cmd`
8
+ * without a shell since the 2024 batfile argument-injection fix. Measured on
9
+ * Windows 11 with dsh 0.1.1-rc.2 (2026-09-02): the bare name gives ENOENT,
10
+ * the resolved `.cmd` shim throws EINVAL *synchronously* out of `spawn()`.
11
+ *
12
+ * `shell: true` — what the shim exists for, and what dsh's own pnpm spawn
13
+ * uses — is not available to us. node hands cmd.exe a joined, UNQUOTED
14
+ * command string, and our argv carries catalog data: a `github:` entry's spec
15
+ * is `github:owner/slug#<sha>&path:<subdir>`, `&` is a cmd command separator,
16
+ * and `subdir` comes from npm. That is a command-injection surface on
17
+ * untrusted input, so the shop resolves the CLI's JS entry and runs it through
18
+ * node instead — no shell, and node escapes the argv itself.
19
+ *
20
+ * Pure core, impure shell: `dshCommand` decides, `resolveDshScript` takes the
21
+ * process snapshot and a filesystem seam as parameters (the same shape as
22
+ * `detectSupervisor(env, proc)` and `RepoPinFs`).
23
+ */
24
+ /** The npm package the dsh CLI is published as. */
25
+ export declare const DSH_PACKAGE = "@deepseek-ai/dsh";
26
+ export interface DshCliFs {
27
+ exists: (path: string) => boolean;
28
+ read: (path: string) => string;
29
+ }
30
+ /** A spawnable command: `spawn(command, args)` with no shell. */
31
+ export interface DshCommand {
32
+ command: string;
33
+ args: string[];
34
+ }
35
+ /**
36
+ * The command that starts the dsh CLI with `args`.
37
+ *
38
+ * The node route replaces exactly one thing: looking the bare name `dsh` up
39
+ * on PATH, which on Windows can never succeed. A `dshBin` naming a specific
40
+ * file is the caller's explicit choice and is spawned as given — honoring it
41
+ * and reporting the real failure beats silently running something else.
42
+ */
43
+ export declare function dshCommand(options: {
44
+ dshBin: string;
45
+ args: readonly string[];
46
+ platform: NodeJS.Platform;
47
+ execPath: string;
48
+ script: string | null;
49
+ }): DshCommand;
50
+ /**
51
+ * Locate the dsh CLI's JS entry, or null when it cannot be found.
52
+ *
53
+ * `argv1` first: the shop is loaded BY dsh, so the running script is dsh's own
54
+ * entry, and reusing it guarantees the child is the same dsh that is serving
55
+ * the shop rather than whichever one PATH happens to name. The owner check is
56
+ * what makes this safe to try — under a test runner `argv1` belongs to the
57
+ * runner, and accepting it would spawn the wrong program.
58
+ *
59
+ * Then the shims on PATH: npm installs the package into `node_modules` beside
60
+ * them, so the manifest is one join away. This is the branch a test harness
61
+ * takes, which is why the real-installation test exercises it.
62
+ */
63
+ export declare function resolveDshScript(fs: DshCliFs, options: {
64
+ argv1: string | undefined;
65
+ path: string | undefined;
66
+ }): string | null;