dshmarket 1.61.0 → 1.63.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/client/client.js +18 -3
- package/lib/check.js +39 -20
- package/lib/dsh-cli.js +72 -14
- package/lib/pnpm-compat.js +15 -0
- package/lib/types/dsh-cli.d.ts +8 -30
- package/lib/types/pnpm-compat.d.ts +1 -1
- package/package.json +1 -1
- package/src/check.ts +39 -21
- package/src/client/MarketSection.tsx +19 -3
- package/src/client/locales.ts +7 -0
- package/src/dsh-cli.ts +71 -13
- package/src/pnpm-compat.ts +16 -1
package/client/client.js
CHANGED
|
@@ -436,6 +436,8 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
436
436
|
sortDownloads: "npm 下载量(近 30 天)",
|
|
437
437
|
/** Discover/theme card byline tooltip for catalog `version`. */
|
|
438
438
|
catalogNpmLatest: "npm 当前 latest",
|
|
439
|
+
catalogVersionDated: "目录数据({0} 刷新)里记录的 npm 版本;在那之后发布的版本要等下一次目录刷新才会出现",
|
|
440
|
+
catalogVersionUndated: "目录数据里记录的 npm 版本;目录每日刷新,所以可能滞后于 npm 上的实际 latest",
|
|
439
441
|
sortStars: "Star 数",
|
|
440
442
|
sortAdded: "发布时间",
|
|
441
443
|
sortDesc: "降序",
|
|
@@ -1024,6 +1026,8 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
1024
1026
|
sortDownloads: "npm downloads (30d)",
|
|
1025
1027
|
/** Discover/theme card byline tooltip for catalog `version`. */
|
|
1026
1028
|
catalogNpmLatest: "npm latest",
|
|
1029
|
+
catalogVersionDated: "the npm version as of the catalog's last refresh ({0}); a release published since then appears after the next refresh",
|
|
1030
|
+
catalogVersionUndated: "the npm version as of the catalog's last refresh — the catalog updates daily, so this can lag npm's actual latest",
|
|
1027
1031
|
sortStars: "Stars",
|
|
1028
1032
|
sortAdded: "Release date",
|
|
1029
1033
|
sortDesc: "Descending",
|
|
@@ -7175,6 +7179,15 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
7175
7179
|
* first reply always has to ask which one it was.
|
|
7176
7180
|
*/
|
|
7177
7181
|
const [version, setVersion] = (0, react.useState)(null);
|
|
7182
|
+
/**
|
|
7183
|
+
* The catalog's own build date (#712). The version on a card is the
|
|
7184
|
+
* catalog's copy, not a live npm lookup, so a plugin published after the
|
|
7185
|
+
* last refresh shows the older number — the tooltip has to say so, or
|
|
7186
|
+
* "npm latest" beside a number that is not npm's latest is a bug report
|
|
7187
|
+
* waiting to be filed. Null until the catalog answers.
|
|
7188
|
+
*/
|
|
7189
|
+
const [catalogUpdated, setCatalogUpdated] = (0, react.useState)(null);
|
|
7190
|
+
const catalogVersionTip = (0, react.useMemo)(() => catalogUpdated === null ? t("catalogVersionUndated") : t("catalogVersionDated").replace("{0}", catalogUpdated), [catalogUpdated, t]);
|
|
7178
7191
|
/** Non-live activation results from the last operation, shown as a banner. */
|
|
7179
7192
|
const [activationWarnings, setActivationWarnings] = (0, react.useState)([]);
|
|
7180
7193
|
const [hostDependencyFindings, setHostDependencyFindings] = (0, react.useState)([]);
|
|
@@ -7363,6 +7376,8 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
7363
7376
|
}
|
|
7364
7377
|
cachedRegistry = body.registry;
|
|
7365
7378
|
setData(body.registry);
|
|
7379
|
+
const catalogDate = body.registry.updated;
|
|
7380
|
+
setCatalogUpdated(typeof catalogDate === "string" && catalogDate.trim() !== "" ? catalogDate : null);
|
|
7366
7381
|
setHostVersion(typeof body.hostVersion === "string" ? body.hostVersion : null);
|
|
7367
7382
|
setLoadError(null);
|
|
7368
7383
|
}).catch((error) => {
|
|
@@ -9119,7 +9134,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
9119
9134
|
}),
|
|
9120
9135
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CatalogVersionMark, {
|
|
9121
9136
|
version: p.version,
|
|
9122
|
-
tip:
|
|
9137
|
+
tip: catalogVersionTip
|
|
9123
9138
|
}),
|
|
9124
9139
|
typeof p.downloads === "number" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
|
|
9125
9140
|
label: String(p.downloads),
|
|
@@ -9296,7 +9311,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
9296
9311
|
}),
|
|
9297
9312
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CatalogVersionMark, {
|
|
9298
9313
|
version: p.version,
|
|
9299
|
-
tip:
|
|
9314
|
+
tip: catalogVersionTip
|
|
9300
9315
|
}),
|
|
9301
9316
|
typeof p.downloads === "number" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
|
|
9302
9317
|
label: String(p.downloads),
|
|
@@ -11446,7 +11461,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
11446
11461
|
}),
|
|
11447
11462
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CatalogVersionMark, {
|
|
11448
11463
|
version: confirming.version,
|
|
11449
|
-
tip:
|
|
11464
|
+
tip: catalogVersionTip
|
|
11450
11465
|
}),
|
|
11451
11466
|
typeof confirming.downloads === "number" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
|
|
11452
11467
|
label: String(confirming.downloads),
|
package/lib/check.js
CHANGED
|
@@ -33,13 +33,20 @@ import { findDshInstallDir } from './dsh-install.js';
|
|
|
33
33
|
import { resolveDshHome } from './home-paths.js';
|
|
34
34
|
import { INBOX_BUNDLES, readBundleRules, suggestOrder, validateOrder } from './order.js';
|
|
35
35
|
// Electron's app.asar packages can be loadable by the host while invisible to
|
|
36
|
-
// filesystem probes made from a profile plugin.
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
// filesystem probes made from a profile plugin. This is a property of the
|
|
37
|
+
// LAYOUT, not of any particular package: inside an archive every probe
|
|
38
|
+
// answers "absent" for every name, so the fallback keys on the layout, on
|
|
39
|
+
// whether the name is DeepSeek's (`@deepseek-ai/` — only they publish the
|
|
40
|
+
// host), and on nothing else.
|
|
41
|
+
//
|
|
42
|
+
// It used to be two fixed name lists — `dsh-experimental-agent-team-profile`
|
|
43
|
+
// as a bundle, `dsh-mcp-client` as a loader — and every name a Desktop build
|
|
44
|
+
// shipped that was not on them reproduced the same false "will fail to boot"
|
|
45
|
+
// (#676: two reporters, `dsh-experimental-agent-team-profile` AND
|
|
46
|
+
// `dsh-experimental-agent-team-web-profile`). A list of which bundles a host
|
|
47
|
+
// ships cannot be maintained from inside a market that cannot see the host.
|
|
41
48
|
function isPackagedDesktopInstall(dshInstallDir) {
|
|
42
|
-
return dshInstallDir !== null && /[/\\]app\.asar[/\\]
|
|
49
|
+
return dshInstallDir !== null && /[/\\]app\.asar[/\\]/iu.test(dshInstallDir);
|
|
43
50
|
}
|
|
44
51
|
export { findDshInstallDir } from './dsh-install.js';
|
|
45
52
|
/** js-yaml dialect for `!!js` scalars — identical to dsh-app-boot's entryListSchema. */
|
|
@@ -711,8 +718,9 @@ function lockfileCoreVersions(profileDir) {
|
|
|
711
718
|
*/
|
|
712
719
|
export function buildBundleLayers(profileDirectory, bundleNames, specs, dshInstallDir) {
|
|
713
720
|
const bundles = bundleNames.map((name) => {
|
|
721
|
+
const officialScope = name.startsWith('@deepseek-ai/');
|
|
714
722
|
const hostProvided = INBOX_BUNDLES.has(name)
|
|
715
|
-
|| (isPackagedDesktopInstall(dshInstallDir) &&
|
|
723
|
+
|| (isPackagedDesktopInstall(dshInstallDir) && officialScope);
|
|
716
724
|
// The real loader gives the DSH installation first refusal for in-box
|
|
717
725
|
// bundles. Desktop keeps that installation private from plugins, so a
|
|
718
726
|
// DIRECT profile-local copy with the same official name is only a stale
|
|
@@ -760,18 +768,23 @@ export function buildBundleLayers(profileDirectory, bundleNames, specs, dshInsta
|
|
|
760
768
|
// --dump-config` on the same profile exited 0. Unknown has to read as
|
|
761
769
|
// unknown; the profile's own bundles are still judged normally.
|
|
762
770
|
//
|
|
763
|
-
// The same holds for
|
|
764
|
-
//
|
|
765
|
-
//
|
|
766
|
-
//
|
|
767
|
-
//
|
|
768
|
-
//
|
|
769
|
-
//
|
|
770
|
-
//
|
|
771
|
-
//
|
|
772
|
-
//
|
|
773
|
-
//
|
|
774
|
-
|
|
771
|
+
// The same holds for an official bundle (#676). Two independent reasons
|
|
772
|
+
// one can be absent from every probe and still be supplied by the
|
|
773
|
+
// running host:
|
|
774
|
+
//
|
|
775
|
+
// - the installation is out of sight entirely — `dshHostInfo` found
|
|
776
|
+
// no anchor at all, which is what a packaged Desktop looks like
|
|
777
|
+
// from in here (#553, and the reporter who could not produce a
|
|
778
|
+
// `dsh --dump-config` because there is no `dsh` on PATH at all);
|
|
779
|
+
// - we have an anchor but it is an ARCHIVE, where a filesystem probe
|
|
780
|
+
// answers "absent" for every name, including the host's own.
|
|
781
|
+
//
|
|
782
|
+
// `hostProvided` is the second case; the first is the check below. Both
|
|
783
|
+
// are gaps in what this process can see, not defects in the profile, so
|
|
784
|
+
// both read as unknown. Community bundles are untouched by this: they
|
|
785
|
+
// resolve through the profile's own node_modules ancestry, which this
|
|
786
|
+
// process CAN probe, so a missing one stays fatal.
|
|
787
|
+
if (hostProvided || (officialScope && dshInstallDir === null)) {
|
|
775
788
|
layer.error = null;
|
|
776
789
|
layer.unresolvedInbox = true;
|
|
777
790
|
return layer;
|
|
@@ -991,7 +1004,13 @@ export function analyzeProfile(profileDirectory, options = {}) {
|
|
|
991
1004
|
if (dshInstall !== null && core.has(packageName)
|
|
992
1005
|
&& resolvePackageDir(join(dshInstall, 'package.json'), packageName) !== null)
|
|
993
1006
|
continue;
|
|
994
|
-
|
|
1007
|
+
// Same rule as the bundle stack above: inside an archive the probe is
|
|
1008
|
+
// blind for EVERY `@deepseek-ai/` name, so the verdict depends on the
|
|
1009
|
+
// name being DeepSeek's and not on whether it appears on a list of the
|
|
1010
|
+
// ones we happen to have seen. `dsh-mcp-client` was that list's only
|
|
1011
|
+
// entry, and the next host-shipped loader would have been a fresh fatal
|
|
1012
|
+
// error (#676).
|
|
1013
|
+
if (isPackagedDesktopInstall(dshInstall) && packageName.startsWith('@deepseek-ai/')) {
|
|
995
1014
|
warnings.push(`${row.layer}: bundled Desktop loader ${packageName} could not be independently resolved from app.asar`);
|
|
996
1015
|
continue;
|
|
997
1016
|
}
|
package/lib/dsh-cli.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Installs run through node:child_process, not ctx.shell: the shell service is
|
|
7
7
|
* the agent's sandboxed executor and denies writes to the profile directory.
|
|
8
8
|
*/
|
|
9
|
-
import { spawn } from 'node:child_process';
|
|
9
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
10
10
|
import { existsSync } from 'node:fs';
|
|
11
11
|
import { homedir } from 'node:os';
|
|
12
12
|
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
@@ -215,20 +215,78 @@ export function toolSearchDirs(platform = process.platform, env = process.env, h
|
|
|
215
215
|
* precisely the branch that cannot work from a spawned child.
|
|
216
216
|
*
|
|
217
217
|
* Scope, stated plainly because it is narrower than the issue title
|
|
218
|
-
* suggests: this is the HTTPS half
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
218
|
+
* suggests: this is the HTTPS half, and the FIRST attempt HTTPS is. Measured
|
|
219
|
+
* with a `GIT_SSH_COMMAND` sentinel that always fails: pnpm's clone of a
|
|
220
|
+
* `github:owner/repo#path:/sub` spec succeeds without ever calling it, so
|
|
221
|
+
* ssh is the fallback that runs after HTTPS fails — which is the private
|
|
222
|
+
* repository case, not the common one.
|
|
223
|
+
*
|
|
224
|
+
* The ssh half (#596, @JINITAIMI121) is a policy decision, and it is made
|
|
225
|
+
* here: `BatchMode=yes`, so a passphrase or host-key question fails fast
|
|
226
|
+
* instead of waiting on a terminal nobody is watching. The cost is real and
|
|
227
|
+
* is why it is conditional — `BatchMode` also disables `SSH_ASKPASS`, so a
|
|
228
|
+
* key that NEEDS a passphrase stops prompting even where something could
|
|
229
|
+
* have answered, and the install fails where it used to work.
|
|
230
|
+
*
|
|
231
|
+
* So it is set only when the user has expressed NO ssh preference at all:
|
|
232
|
+
* `GIT_SSH_COMMAND` and `GIT_SSH` in the environment, and `core.sshCommand`
|
|
233
|
+
* in git config — the three ordinary ways to choose an identity, and
|
|
234
|
+
* `GIT_SSH_COMMAND` would silently override all three (measured: with
|
|
235
|
+
* `core.sshCommand` set, ours wins and theirs is never run). A user who
|
|
236
|
+
* has an agent or a specific key configured keeps exactly what they have;
|
|
237
|
+
* the prompt is closed for everyone else, and `classifyPnpmFailure` says
|
|
238
|
+
* what to do if that turns out to be the passphrase case.
|
|
227
239
|
*/
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
240
|
+
let coreSshCommandProbe;
|
|
241
|
+
/**
|
|
242
|
+
* `core.sshCommand` from the user's git configuration, or null.
|
|
243
|
+
*
|
|
244
|
+
* The third place an ssh identity hides, and the one the environment cannot
|
|
245
|
+
* show: `git config --get` answers it. Memoized for the process — a user
|
|
246
|
+
* does not reconfigure git mid-install, and this runs on every spawn.
|
|
247
|
+
*
|
|
248
|
+
* @returns the configured command, or null when git has none (including
|
|
249
|
+
* when git is absent — an unreadable answer is not a choice).
|
|
250
|
+
*/
|
|
251
|
+
export function probeCoreSshCommand(env) {
|
|
252
|
+
// Memoized only for the caller that has no opinion: a test that supplies
|
|
253
|
+
// its own environment is asking a different question and must get the
|
|
254
|
+
// fresh answer.
|
|
255
|
+
if (env === undefined && coreSshCommandProbe !== undefined)
|
|
256
|
+
return coreSshCommandProbe ?? null;
|
|
257
|
+
try {
|
|
258
|
+
const out = spawnSync('git', ['config', '--get', 'core.sshCommand'], {
|
|
259
|
+
encoding: 'utf8',
|
|
260
|
+
timeout: 5_000,
|
|
261
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
262
|
+
...(env === undefined ? {} : { env }),
|
|
263
|
+
// A user's git may be a shim that prints to a console; nothing here
|
|
264
|
+
// wants a window.
|
|
265
|
+
windowsHide: true,
|
|
266
|
+
});
|
|
267
|
+
const answer = out.status === 0 && typeof out.stdout === 'string' && out.stdout.trim() !== ''
|
|
268
|
+
? out.stdout.trim()
|
|
269
|
+
: null;
|
|
270
|
+
if (env === undefined)
|
|
271
|
+
coreSshCommandProbe = answer;
|
|
272
|
+
return answer;
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
if (env === undefined)
|
|
276
|
+
coreSshCommandProbe = null;
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
export function gitEnvForPnpm(env = process.env, coreSshCommand = probeCoreSshCommand()) {
|
|
281
|
+
const out = {};
|
|
282
|
+
// Blank counts as unset: an empty value is not a setting git can parse.
|
|
283
|
+
const speaks = (value) => (value ?? '').trim() !== '';
|
|
284
|
+
if (!speaks(env.GIT_TERMINAL_PROMPT))
|
|
285
|
+
out.GIT_TERMINAL_PROMPT = '0';
|
|
286
|
+
const choseElsewhere = speaks(env.GIT_SSH_COMMAND) || speaks(env.GIT_SSH) || speaks(coreSshCommand ?? '');
|
|
287
|
+
if (!choseElsewhere)
|
|
288
|
+
out.GIT_SSH_COMMAND = 'ssh -oBatchMode=yes';
|
|
289
|
+
return out;
|
|
232
290
|
}
|
|
233
291
|
/**
|
|
234
292
|
* Every `--config.<key>=<value>` override in the argv, repeated as the
|
package/lib/pnpm-compat.js
CHANGED
|
@@ -344,6 +344,21 @@ export function classifyPnpmFailure(output, exitCode) {
|
|
|
344
344
|
};
|
|
345
345
|
}
|
|
346
346
|
}
|
|
347
|
+
// #596: the ssh half of #587. git asks for a passphrase (or a host-key
|
|
348
|
+
// confirmation) on a terminal a spawned child does not have; the market
|
|
349
|
+
// closes that prompt with `BatchMode=yes` so the question becomes a fast
|
|
350
|
+
// failure instead of a fifteen-minute hang. This is that failure, and it
|
|
351
|
+
// needs its own message because git's own words send the reader to the
|
|
352
|
+
// wrong place: `Permission denied (publickey)` reads as "your key is
|
|
353
|
+
// wrong", and the key is usually fine — it wants a passphrase, and the
|
|
354
|
+
// channel that would have asked for it is exactly what was shut.
|
|
355
|
+
if (/Permission denied \(publickey\)|Could not read from remote repository/.test(output)) {
|
|
356
|
+
return {
|
|
357
|
+
code: 'ssh-auth-failed',
|
|
358
|
+
recoverable: false,
|
|
359
|
+
message: 'git 无法在无人值守的情况下完成 SSH 认证。如果你的 SSH key 设了密码,请用 ssh-agent(ssh-add),或自己设置 GIT_SSH_COMMAND 指向你的命令,市场不会覆盖它。 / git could not complete SSH authentication unattended. If your SSH key has a passphrase, use ssh-agent (ssh-add), or set GIT_SSH_COMMAND to your own command — the market leaves yours alone.',
|
|
360
|
+
};
|
|
361
|
+
}
|
|
347
362
|
if (output.includes('ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED')) {
|
|
348
363
|
return {
|
|
349
364
|
code: 'git-prepare-not-allowed',
|
package/lib/types/dsh-cli.d.ts
CHANGED
|
@@ -79,39 +79,17 @@ export declare function proxyEnvForPnpm(env?: NodeJS.ProcessEnv, region?: Region
|
|
|
79
79
|
*/
|
|
80
80
|
export declare function toolSearchDirs(platform?: string, env?: NodeJS.ProcessEnv, home?: string): string[];
|
|
81
81
|
/**
|
|
82
|
-
*
|
|
83
|
-
* (#587).
|
|
82
|
+
* `core.sshCommand` from the user's git configuration, or null.
|
|
84
83
|
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
* path `accelerate.ts` describes: `github:owner/repo#path:/sub` is one, and
|
|
89
|
-
* pnpm really does shell out to `git` for it, trying HTTPS first.
|
|
84
|
+
* The third place an ssh identity hides, and the one the environment cannot
|
|
85
|
+
* show: `git config --get` answers it. Memoized for the process — a user
|
|
86
|
+
* does not reconfigure git mid-install, and this runs on every spawn.
|
|
90
87
|
*
|
|
91
|
-
*
|
|
92
|
-
* is
|
|
93
|
-
* the reporter caught `git.exe` alive for eight minutes having burned 0.05s
|
|
94
|
-
* of CPU, and only the fifteen-minute install timeout ended it. Refusing
|
|
95
|
-
* the prompt turns that into a fast, readable failure.
|
|
96
|
-
*
|
|
97
|
-
* It is a default, not an override: a value the caller set wins, and blank
|
|
98
|
-
* counts as unset because an empty `GIT_TERMINAL_PROMPT` is not a setting
|
|
99
|
-
* git can parse either. Credential helpers and `GIT_ASKPASS` are untouched
|
|
100
|
-
* and still answer first — this closes only the terminal fallback, which is
|
|
101
|
-
* precisely the branch that cannot work from a spawned child.
|
|
102
|
-
*
|
|
103
|
-
* Scope, stated plainly because it is narrower than the issue title
|
|
104
|
-
* suggests: this is the HTTPS half. pnpm falls back to `git@github.com:`
|
|
105
|
-
* when HTTPS fails, and the ssh side needs `GIT_SSH_COMMAND`, which
|
|
106
|
-
* overrides `core.sshCommand` and `GIT_SSH` — the two ordinary ways to
|
|
107
|
-
* choose an identity — and whose `BatchMode=yes` would disable
|
|
108
|
-
* `SSH_ASKPASS`, breaking key-passphrase installs that work today. That
|
|
109
|
-
* half needs a policy decision, so it is not made here. Note also that on
|
|
110
|
-
* POSIX `runDshPlugin` spawns detached, so the subtree has no controlling
|
|
111
|
-
* terminal and the prompt already dies instantly; the hang the issue
|
|
112
|
-
* reports needs Windows, where the spawn is not detached.
|
|
88
|
+
* @returns the configured command, or null when git has none (including
|
|
89
|
+
* when git is absent — an unreadable answer is not a choice).
|
|
113
90
|
*/
|
|
114
|
-
export declare function
|
|
91
|
+
export declare function probeCoreSshCommand(env?: NodeJS.ProcessEnv): string | null;
|
|
92
|
+
export declare function gitEnvForPnpm(env?: NodeJS.ProcessEnv, coreSshCommand?: string | null): NodeJS.ProcessEnv;
|
|
115
93
|
/**
|
|
116
94
|
* Every `--config.<key>=<value>` override in the argv, repeated as the
|
|
117
95
|
* `PNPM_CONFIG_<KEY>` environment variable that pnpm 12 still reads.
|
|
@@ -33,7 +33,7 @@ export declare function pluginArgsFor(profileDir: string, pluginArgs: string[]):
|
|
|
33
33
|
*/
|
|
34
34
|
export declare const HOST_NAMESPACE_RE: RegExp;
|
|
35
35
|
export interface PnpmFailure {
|
|
36
|
-
code: 'adding-to-root' | 'not-a-workspace' | 'hoist-pattern-diff' | 'pnpm-missing' | 'release-age-violation' | 'ignored-builds' | 'git-prepare-not-allowed' | 'git-prepare-failed' | 'tarball-url-mismatch' | 'fetch-404' | 'no-matching-version' | 'transient-network' | 'fetch-timeout' | 'unexpected-store' | 'patch-failed' | 'missing-tarball-integrity' | 'windows-file-locked' | 'pnpm-unusable' | 'missing-local-dependency' | 'unparseable-build-key' | 'native-oom';
|
|
36
|
+
code: 'adding-to-root' | 'not-a-workspace' | 'hoist-pattern-diff' | 'pnpm-missing' | 'release-age-violation' | 'ignored-builds' | 'git-prepare-not-allowed' | 'git-prepare-failed' | 'tarball-url-mismatch' | 'fetch-404' | 'no-matching-version' | 'transient-network' | 'fetch-timeout' | 'unexpected-store' | 'patch-failed' | 'missing-tarball-integrity' | 'windows-file-locked' | 'pnpm-unusable' | 'missing-local-dependency' | 'unparseable-build-key' | 'native-oom' | 'ssh-auth-failed';
|
|
37
37
|
/** Bilingual, actionable message shown to the user instead of the raw wall of text. */
|
|
38
38
|
message: string;
|
|
39
39
|
/** True when re-running `pnpm install` in the profile is the documented recovery. */
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dshmarket",
|
|
3
3
|
"description": "Visual plugin market inside DeepSeek Harness — browse, search, and one-click install community plugins. · DSH 可视化插件市场:逛一逛,点一下,装好。",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.63.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/types/index.d.ts",
|
package/src/check.ts
CHANGED
|
@@ -35,14 +35,20 @@ import { resolveDshHome } from './home-paths.ts'
|
|
|
35
35
|
import { INBOX_BUNDLES, readBundleRules, suggestOrder, validateOrder } from './order.ts'
|
|
36
36
|
|
|
37
37
|
// Electron's app.asar packages can be loadable by the host while invisible to
|
|
38
|
-
// filesystem probes made from a profile plugin.
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
38
|
+
// filesystem probes made from a profile plugin. This is a property of the
|
|
39
|
+
// LAYOUT, not of any particular package: inside an archive every probe
|
|
40
|
+
// answers "absent" for every name, so the fallback keys on the layout, on
|
|
41
|
+
// whether the name is DeepSeek's (`@deepseek-ai/` — only they publish the
|
|
42
|
+
// host), and on nothing else.
|
|
43
|
+
//
|
|
44
|
+
// It used to be two fixed name lists — `dsh-experimental-agent-team-profile`
|
|
45
|
+
// as a bundle, `dsh-mcp-client` as a loader — and every name a Desktop build
|
|
46
|
+
// shipped that was not on them reproduced the same false "will fail to boot"
|
|
47
|
+
// (#676: two reporters, `dsh-experimental-agent-team-profile` AND
|
|
48
|
+
// `dsh-experimental-agent-team-web-profile`). A list of which bundles a host
|
|
49
|
+
// ships cannot be maintained from inside a market that cannot see the host.
|
|
44
50
|
function isPackagedDesktopInstall(dshInstallDir: string | null): boolean {
|
|
45
|
-
return dshInstallDir !== null && /[/\\]app\.asar[/\\]
|
|
51
|
+
return dshInstallDir !== null && /[/\\]app\.asar[/\\]/iu.test(dshInstallDir)
|
|
46
52
|
}
|
|
47
53
|
|
|
48
54
|
export { findDshInstallDir } from './dsh-install.ts'
|
|
@@ -901,8 +907,9 @@ export function buildBundleLayers(
|
|
|
901
907
|
dshInstallDir: string | null,
|
|
902
908
|
): { bundles: BundleLayer[]; layers: LayerInput[] } {
|
|
903
909
|
const bundles: BundleLayer[] = bundleNames.map((name) => {
|
|
910
|
+
const officialScope = name.startsWith('@deepseek-ai/')
|
|
904
911
|
const hostProvided = INBOX_BUNDLES.has(name)
|
|
905
|
-
|| (isPackagedDesktopInstall(dshInstallDir) &&
|
|
912
|
+
|| (isPackagedDesktopInstall(dshInstallDir) && officialScope)
|
|
906
913
|
// The real loader gives the DSH installation first refusal for in-box
|
|
907
914
|
// bundles. Desktop keeps that installation private from plugins, so a
|
|
908
915
|
// DIRECT profile-local copy with the same official name is only a stale
|
|
@@ -948,18 +955,23 @@ export function buildBundleLayers(
|
|
|
948
955
|
// --dump-config` on the same profile exited 0. Unknown has to read as
|
|
949
956
|
// unknown; the profile's own bundles are still judged normally.
|
|
950
957
|
//
|
|
951
|
-
// The same holds for
|
|
952
|
-
//
|
|
953
|
-
//
|
|
954
|
-
//
|
|
955
|
-
//
|
|
956
|
-
//
|
|
957
|
-
//
|
|
958
|
-
//
|
|
959
|
-
//
|
|
960
|
-
//
|
|
961
|
-
//
|
|
962
|
-
|
|
958
|
+
// The same holds for an official bundle (#676). Two independent reasons
|
|
959
|
+
// one can be absent from every probe and still be supplied by the
|
|
960
|
+
// running host:
|
|
961
|
+
//
|
|
962
|
+
// - the installation is out of sight entirely — `dshHostInfo` found
|
|
963
|
+
// no anchor at all, which is what a packaged Desktop looks like
|
|
964
|
+
// from in here (#553, and the reporter who could not produce a
|
|
965
|
+
// `dsh --dump-config` because there is no `dsh` on PATH at all);
|
|
966
|
+
// - we have an anchor but it is an ARCHIVE, where a filesystem probe
|
|
967
|
+
// answers "absent" for every name, including the host's own.
|
|
968
|
+
//
|
|
969
|
+
// `hostProvided` is the second case; the first is the check below. Both
|
|
970
|
+
// are gaps in what this process can see, not defects in the profile, so
|
|
971
|
+
// both read as unknown. Community bundles are untouched by this: they
|
|
972
|
+
// resolve through the profile's own node_modules ancestry, which this
|
|
973
|
+
// process CAN probe, so a missing one stays fatal.
|
|
974
|
+
if (hostProvided || (officialScope && dshInstallDir === null)) {
|
|
963
975
|
layer.error = null
|
|
964
976
|
layer.unresolvedInbox = true
|
|
965
977
|
return layer
|
|
@@ -1174,7 +1186,13 @@ export function analyzeProfile(profileDirectory: string, options: CheckOptions =
|
|
|
1174
1186
|
// the writable profile. The loader can resolve them from this anchor.
|
|
1175
1187
|
if (dshInstall !== null && core.has(packageName)
|
|
1176
1188
|
&& resolvePackageDir(join(dshInstall, 'package.json'), packageName) !== null) continue
|
|
1177
|
-
|
|
1189
|
+
// Same rule as the bundle stack above: inside an archive the probe is
|
|
1190
|
+
// blind for EVERY `@deepseek-ai/` name, so the verdict depends on the
|
|
1191
|
+
// name being DeepSeek's and not on whether it appears on a list of the
|
|
1192
|
+
// ones we happen to have seen. `dsh-mcp-client` was that list's only
|
|
1193
|
+
// entry, and the next host-shipped loader would have been a fresh fatal
|
|
1194
|
+
// error (#676).
|
|
1195
|
+
if (isPackagedDesktopInstall(dshInstall) && packageName.startsWith('@deepseek-ai/')) {
|
|
1178
1196
|
warnings.push(`${row.layer}: bundled Desktop loader ${packageName} could not be independently resolved from app.asar`)
|
|
1179
1197
|
continue
|
|
1180
1198
|
}
|
|
@@ -1527,6 +1527,20 @@ export function MarketSection(props: MarketSectionProps) {
|
|
|
1527
1527
|
* first reply always has to ask which one it was.
|
|
1528
1528
|
*/
|
|
1529
1529
|
const [version, setVersion] = useState<string | null>(null)
|
|
1530
|
+
/**
|
|
1531
|
+
* The catalog's own build date (#712). The version on a card is the
|
|
1532
|
+
* catalog's copy, not a live npm lookup, so a plugin published after the
|
|
1533
|
+
* last refresh shows the older number — the tooltip has to say so, or
|
|
1534
|
+
* "npm latest" beside a number that is not npm's latest is a bug report
|
|
1535
|
+
* waiting to be filed. Null until the catalog answers.
|
|
1536
|
+
*/
|
|
1537
|
+
const [catalogUpdated, setCatalogUpdated] = useState<string | null>(null)
|
|
1538
|
+
const catalogVersionTip = useMemo(
|
|
1539
|
+
() => (catalogUpdated === null
|
|
1540
|
+
? t('catalogVersionUndated')
|
|
1541
|
+
: t('catalogVersionDated').replace('{0}', catalogUpdated)),
|
|
1542
|
+
[catalogUpdated, t],
|
|
1543
|
+
)
|
|
1530
1544
|
/** Non-live activation results from the last operation, shown as a banner. */
|
|
1531
1545
|
const [activationWarnings, setActivationWarnings] = useState<{ name: string; info: ActivationInfo }[]>([])
|
|
1532
1546
|
const [hostDependencyFindings, setHostDependencyFindings] = useState<SharedHostPackageDependencyFinding[]>([])
|
|
@@ -1739,6 +1753,8 @@ export function MarketSection(props: MarketSectionProps) {
|
|
|
1739
1753
|
}
|
|
1740
1754
|
cachedRegistry = body.registry
|
|
1741
1755
|
setData(body.registry)
|
|
1756
|
+
const catalogDate = body.registry.updated
|
|
1757
|
+
setCatalogUpdated(typeof catalogDate === 'string' && catalogDate.trim() !== '' ? catalogDate : null)
|
|
1742
1758
|
setHostVersion(typeof body.hostVersion === 'string' ? body.hostVersion : null)
|
|
1743
1759
|
setLoadError(null)
|
|
1744
1760
|
})
|
|
@@ -3579,7 +3595,7 @@ export function MarketSection(props: MarketSectionProps) {
|
|
|
3579
3595
|
<div className={css.byline}>
|
|
3580
3596
|
<OwnerAvatar name={p.name} owner={p.owner || ''} />
|
|
3581
3597
|
<span className={css.owner} title={p.owner}>{p.owner}</span>
|
|
3582
|
-
<CatalogVersionMark version={p.version} tip={
|
|
3598
|
+
<CatalogVersionMark version={p.version} tip={catalogVersionTip} />
|
|
3583
3599
|
{typeof p.downloads === 'number' && (
|
|
3584
3600
|
<Tooltip label={String(p.downloads)} side="top">
|
|
3585
3601
|
<span className={css.star}>{'· ↓ ' + formatCount(p.downloads)}</span>
|
|
@@ -3721,7 +3737,7 @@ export function MarketSection(props: MarketSectionProps) {
|
|
|
3721
3737
|
<div className={css.byline}>
|
|
3722
3738
|
<OwnerAvatar name={p.name} owner={p.owner || ''} />
|
|
3723
3739
|
<span className={css.owner} title={p.owner}>{p.owner}</span>
|
|
3724
|
-
<CatalogVersionMark version={p.version} tip={
|
|
3740
|
+
<CatalogVersionMark version={p.version} tip={catalogVersionTip} />
|
|
3725
3741
|
{typeof p.downloads === 'number' && (
|
|
3726
3742
|
<Tooltip label={String(p.downloads)} side="top">
|
|
3727
3743
|
<span className={css.star}>{'· ↓ ' + formatCount(p.downloads)}</span>
|
|
@@ -5473,7 +5489,7 @@ export function MarketSection(props: MarketSectionProps) {
|
|
|
5473
5489
|
<div className={css.byline}>
|
|
5474
5490
|
<OwnerAvatar name={confirming.name} owner={confirming.owner || ''} />
|
|
5475
5491
|
<span className={css.owner} title={confirming.owner}>{confirming.owner}</span>
|
|
5476
|
-
<CatalogVersionMark version={confirming.version} tip={
|
|
5492
|
+
<CatalogVersionMark version={confirming.version} tip={catalogVersionTip} />
|
|
5477
5493
|
{typeof confirming.downloads === 'number' && (
|
|
5478
5494
|
<Tooltip label={String(confirming.downloads)} side="top">
|
|
5479
5495
|
<span className={css.star}>{'· ↓ ' + formatCount(confirming.downloads)}</span>
|
package/src/client/locales.ts
CHANGED
|
@@ -245,6 +245,11 @@ export const zh = {
|
|
|
245
245
|
sortDownloads: 'npm 下载量(近 30 天)',
|
|
246
246
|
/** Discover/theme card byline tooltip for catalog `version`. */
|
|
247
247
|
catalogNpmLatest: 'npm 当前 latest',
|
|
248
|
+
// #712: the number is the CATALOG's copy, refreshed daily — not a live npm
|
|
249
|
+
// lookup. Saying "npm latest" beside a number that is not npm's latest is
|
|
250
|
+
// how a stale display turns into a bug report.
|
|
251
|
+
catalogVersionDated: '目录数据({0} 刷新)里记录的 npm 版本;在那之后发布的版本要等下一次目录刷新才会出现',
|
|
252
|
+
catalogVersionUndated: '目录数据里记录的 npm 版本;目录每日刷新,所以可能滞后于 npm 上的实际 latest',
|
|
248
253
|
sortStars: 'Star 数',
|
|
249
254
|
sortAdded: '发布时间',
|
|
250
255
|
sortDesc: '降序',
|
|
@@ -841,6 +846,8 @@ export const en: Record<MarketKey, string> = {
|
|
|
841
846
|
sortDownloads: 'npm downloads (30d)',
|
|
842
847
|
/** Discover/theme card byline tooltip for catalog `version`. */
|
|
843
848
|
catalogNpmLatest: 'npm latest',
|
|
849
|
+
catalogVersionDated: "the npm version as of the catalog's last refresh ({0}); a release published since then appears after the next refresh",
|
|
850
|
+
catalogVersionUndated: "the npm version as of the catalog's last refresh — the catalog updates daily, so this can lag npm's actual latest",
|
|
844
851
|
sortStars: 'Stars',
|
|
845
852
|
sortAdded: 'Release date',
|
|
846
853
|
sortDesc: 'Descending',
|
package/src/dsh-cli.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* the agent's sandboxed executor and denies writes to the profile directory.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { spawn } from 'node:child_process'
|
|
10
|
+
import { spawn, spawnSync } from 'node:child_process'
|
|
11
11
|
import type { ChildProcess, SpawnOptions } from 'node:child_process'
|
|
12
12
|
import { existsSync } from 'node:fs'
|
|
13
13
|
import { homedir } from 'node:os'
|
|
@@ -217,19 +217,77 @@ export function toolSearchDirs(
|
|
|
217
217
|
* precisely the branch that cannot work from a spawned child.
|
|
218
218
|
*
|
|
219
219
|
* Scope, stated plainly because it is narrower than the issue title
|
|
220
|
-
* suggests: this is the HTTPS half
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
220
|
+
* suggests: this is the HTTPS half, and the FIRST attempt HTTPS is. Measured
|
|
221
|
+
* with a `GIT_SSH_COMMAND` sentinel that always fails: pnpm's clone of a
|
|
222
|
+
* `github:owner/repo#path:/sub` spec succeeds without ever calling it, so
|
|
223
|
+
* ssh is the fallback that runs after HTTPS fails — which is the private
|
|
224
|
+
* repository case, not the common one.
|
|
225
|
+
*
|
|
226
|
+
* The ssh half (#596, @JINITAIMI121) is a policy decision, and it is made
|
|
227
|
+
* here: `BatchMode=yes`, so a passphrase or host-key question fails fast
|
|
228
|
+
* instead of waiting on a terminal nobody is watching. The cost is real and
|
|
229
|
+
* is why it is conditional — `BatchMode` also disables `SSH_ASKPASS`, so a
|
|
230
|
+
* key that NEEDS a passphrase stops prompting even where something could
|
|
231
|
+
* have answered, and the install fails where it used to work.
|
|
232
|
+
*
|
|
233
|
+
* So it is set only when the user has expressed NO ssh preference at all:
|
|
234
|
+
* `GIT_SSH_COMMAND` and `GIT_SSH` in the environment, and `core.sshCommand`
|
|
235
|
+
* in git config — the three ordinary ways to choose an identity, and
|
|
236
|
+
* `GIT_SSH_COMMAND` would silently override all three (measured: with
|
|
237
|
+
* `core.sshCommand` set, ours wins and theirs is never run). A user who
|
|
238
|
+
* has an agent or a specific key configured keeps exactly what they have;
|
|
239
|
+
* the prompt is closed for everyone else, and `classifyPnpmFailure` says
|
|
240
|
+
* what to do if that turns out to be the passphrase case.
|
|
241
|
+
*/
|
|
242
|
+
let coreSshCommandProbe: string | null | undefined
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* `core.sshCommand` from the user's git configuration, or null.
|
|
246
|
+
*
|
|
247
|
+
* The third place an ssh identity hides, and the one the environment cannot
|
|
248
|
+
* show: `git config --get` answers it. Memoized for the process — a user
|
|
249
|
+
* does not reconfigure git mid-install, and this runs on every spawn.
|
|
250
|
+
*
|
|
251
|
+
* @returns the configured command, or null when git has none (including
|
|
252
|
+
* when git is absent — an unreadable answer is not a choice).
|
|
229
253
|
*/
|
|
230
|
-
export function
|
|
231
|
-
|
|
232
|
-
|
|
254
|
+
export function probeCoreSshCommand(env?: NodeJS.ProcessEnv): string | null {
|
|
255
|
+
// Memoized only for the caller that has no opinion: a test that supplies
|
|
256
|
+
// its own environment is asking a different question and must get the
|
|
257
|
+
// fresh answer.
|
|
258
|
+
if (env === undefined && coreSshCommandProbe !== undefined) return coreSshCommandProbe ?? null
|
|
259
|
+
try {
|
|
260
|
+
const out = spawnSync('git', ['config', '--get', 'core.sshCommand'], {
|
|
261
|
+
encoding: 'utf8',
|
|
262
|
+
timeout: 5_000,
|
|
263
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
264
|
+
...(env === undefined ? {} : { env }),
|
|
265
|
+
// A user's git may be a shim that prints to a console; nothing here
|
|
266
|
+
// wants a window.
|
|
267
|
+
windowsHide: true,
|
|
268
|
+
})
|
|
269
|
+
const answer = out.status === 0 && typeof out.stdout === 'string' && out.stdout.trim() !== ''
|
|
270
|
+
? out.stdout.trim()
|
|
271
|
+
: null
|
|
272
|
+
if (env === undefined) coreSshCommandProbe = answer
|
|
273
|
+
return answer
|
|
274
|
+
} catch {
|
|
275
|
+
if (env === undefined) coreSshCommandProbe = null
|
|
276
|
+
return null
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export function gitEnvForPnpm(
|
|
281
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
282
|
+
coreSshCommand: string | null = probeCoreSshCommand(),
|
|
283
|
+
): NodeJS.ProcessEnv {
|
|
284
|
+
const out: NodeJS.ProcessEnv = {}
|
|
285
|
+
// Blank counts as unset: an empty value is not a setting git can parse.
|
|
286
|
+
const speaks = (value: string | undefined): boolean => (value ?? '').trim() !== ''
|
|
287
|
+
if (!speaks(env.GIT_TERMINAL_PROMPT)) out.GIT_TERMINAL_PROMPT = '0'
|
|
288
|
+
const choseElsewhere = speaks(env.GIT_SSH_COMMAND) || speaks(env.GIT_SSH) || speaks(coreSshCommand ?? '')
|
|
289
|
+
if (!choseElsewhere) out.GIT_SSH_COMMAND = 'ssh -oBatchMode=yes'
|
|
290
|
+
return out
|
|
233
291
|
}
|
|
234
292
|
|
|
235
293
|
/**
|
package/src/pnpm-compat.ts
CHANGED
|
@@ -48,7 +48,7 @@ export interface PnpmFailure {
|
|
|
48
48
|
| 'ignored-builds' | 'git-prepare-not-allowed' | 'git-prepare-failed' | 'tarball-url-mismatch'
|
|
49
49
|
| 'fetch-404' | 'no-matching-version' | 'transient-network' | 'fetch-timeout'
|
|
50
50
|
| 'unexpected-store' | 'patch-failed' | 'missing-tarball-integrity' | 'windows-file-locked'
|
|
51
|
-
| 'pnpm-unusable' | 'missing-local-dependency' | 'unparseable-build-key' | 'native-oom'
|
|
51
|
+
| 'pnpm-unusable' | 'missing-local-dependency' | 'unparseable-build-key' | 'native-oom' | 'ssh-auth-failed'
|
|
52
52
|
/** Bilingual, actionable message shown to the user instead of the raw wall of text. */
|
|
53
53
|
message: string
|
|
54
54
|
/** True when re-running `pnpm install` in the profile is the documented recovery. */
|
|
@@ -381,6 +381,21 @@ export function classifyPnpmFailure(output: string, exitCode?: number | null): P
|
|
|
381
381
|
}
|
|
382
382
|
}
|
|
383
383
|
}
|
|
384
|
+
// #596: the ssh half of #587. git asks for a passphrase (or a host-key
|
|
385
|
+
// confirmation) on a terminal a spawned child does not have; the market
|
|
386
|
+
// closes that prompt with `BatchMode=yes` so the question becomes a fast
|
|
387
|
+
// failure instead of a fifteen-minute hang. This is that failure, and it
|
|
388
|
+
// needs its own message because git's own words send the reader to the
|
|
389
|
+
// wrong place: `Permission denied (publickey)` reads as "your key is
|
|
390
|
+
// wrong", and the key is usually fine — it wants a passphrase, and the
|
|
391
|
+
// channel that would have asked for it is exactly what was shut.
|
|
392
|
+
if (/Permission denied \(publickey\)|Could not read from remote repository/.test(output)) {
|
|
393
|
+
return {
|
|
394
|
+
code: 'ssh-auth-failed',
|
|
395
|
+
recoverable: false,
|
|
396
|
+
message: 'git 无法在无人值守的情况下完成 SSH 认证。如果你的 SSH key 设了密码,请用 ssh-agent(ssh-add),或自己设置 GIT_SSH_COMMAND 指向你的命令,市场不会覆盖它。 / git could not complete SSH authentication unattended. If your SSH key has a passphrase, use ssh-agent (ssh-add), or set GIT_SSH_COMMAND to your own command — the market leaves yours alone.',
|
|
397
|
+
}
|
|
398
|
+
}
|
|
384
399
|
if (output.includes('ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED')) {
|
|
385
400
|
return {
|
|
386
401
|
code: 'git-prepare-not-allowed',
|