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/client.js +244 -248
- package/lib/index.js +237 -32
- package/lib/typert.host.js +10 -10
- package/lib/typert.remote-client.js +9 -9
- package/lib/types/client/locales.d.ts +0 -2
- package/lib/types/host/dsh-cli.d.ts +66 -0
- package/lib/types/host/executor.d.ts +29 -0
- package/lib/types/host/index.d.ts +8 -4
- package/lib/types/host/install.d.ts +1 -1
- package/lib/types/host/restart.d.ts +7 -1
- package/package.json +1 -1
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
|
|
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$
|
|
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$
|
|
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
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
package/lib/typert.host.js
CHANGED
|
@@ -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("
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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' | '
|
|
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("
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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;
|