dshmarket 1.20.4 → 1.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/dsh-cli.js +52 -1
- package/lib/sources.js +1 -1
- package/lib/types/dsh-cli.d.ts +48 -10
- package/lib/types/sources.d.ts +2 -0
- package/package.json +1 -1
- package/src/dsh-cli.ts +104 -12
- package/src/sources.ts +1 -1
package/lib/dsh-cli.js
CHANGED
|
@@ -15,6 +15,8 @@ import { createProgressTracker } from './ndjson.js';
|
|
|
15
15
|
import { pluginArgsFor } from './pnpm-compat.js';
|
|
16
16
|
import { isDshProfileName, profileDir } from './profile.js';
|
|
17
17
|
import { activeRegion, DEFAULT_NPM_REGISTRY, routesFor } from './regions.js';
|
|
18
|
+
import { NPM_NAME_RE } from './sources.js';
|
|
19
|
+
import { fetchNpmLatest } from './updates.js';
|
|
18
20
|
// 15 min default (slow networks + git installs), overridable for CI/tests.
|
|
19
21
|
// (#6 by @qichuang321.)
|
|
20
22
|
/**
|
|
@@ -279,6 +281,46 @@ export function dshArgv() {
|
|
|
279
281
|
// Bare `dsh` is a .cmd shim on Windows that only a shell can start (#13).
|
|
280
282
|
return { file: 'dsh', args: [], cwd: undefined, viaShell: winCmdShim };
|
|
281
283
|
}
|
|
284
|
+
/** An npm name with a fully pinned version — the only target their boundary takes. */
|
|
285
|
+
const EXACT_NPM_TARGET_RE = /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*@\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
|
|
286
|
+
/**
|
|
287
|
+
* Rewrite an `add` argv into the shape Anywhere Labs' install boundary
|
|
288
|
+
* accepts, or null when it cannot be expressed there.
|
|
289
|
+
*
|
|
290
|
+
* Their validator wants exactly one target of the form `name@1.2.3` — not a
|
|
291
|
+
* bare name, not `@latest`, and not a `github:` source (read from
|
|
292
|
+
* `validateExternalMarketInstallArgs`, dsh-plugin-desktop/src/pnpm.ts). The
|
|
293
|
+
* market sends a bare name for a registry plugin and `dshmarket@latest` for
|
|
294
|
+
* itself, so both need the version resolved before that boundary will take
|
|
295
|
+
* them.
|
|
296
|
+
*
|
|
297
|
+
* Returning null is a normal outcome, not a failure: a github-sourced plugin
|
|
298
|
+
* has no `name@version` spelling at all. The caller falls back to the
|
|
299
|
+
* ordinary path, which on that host reports their own refusal — an accurate
|
|
300
|
+
* message about their contract, rather than one this package invented.
|
|
301
|
+
*/
|
|
302
|
+
async function exactNpmArgs(args) {
|
|
303
|
+
const targets = args.slice(1).filter(argument => !argument.startsWith('-'));
|
|
304
|
+
const target = targets[0];
|
|
305
|
+
if (targets.length !== 1 || target === undefined)
|
|
306
|
+
return null;
|
|
307
|
+
if (EXACT_NPM_TARGET_RE.test(target))
|
|
308
|
+
return [...args];
|
|
309
|
+
// A bare name, or one pinned to a dist-tag. Only a registry package can be
|
|
310
|
+
// resolved; `github:owner/repo` and file paths stop here.
|
|
311
|
+
const at = target.lastIndexOf('@');
|
|
312
|
+
const name = at > 0 ? target.slice(0, at) : target;
|
|
313
|
+
if (!NPM_NAME_RE.test(name))
|
|
314
|
+
return null;
|
|
315
|
+
const version = await fetchNpmLatest(name);
|
|
316
|
+
if (version === null)
|
|
317
|
+
return null;
|
|
318
|
+
const rewritten = `${name}@${version}`;
|
|
319
|
+
if (!EXACT_NPM_TARGET_RE.test(rewritten))
|
|
320
|
+
return null;
|
|
321
|
+
logEvent('info', 'install', `desktop install boundary needs an exact version: ${target} -> ${rewritten}`);
|
|
322
|
+
return args.map(argument => (argument === target ? rewritten : argument));
|
|
323
|
+
}
|
|
282
324
|
/**
|
|
283
325
|
* Kill a spawned child and, on Windows, its whole process tree — `kill()`
|
|
284
326
|
* there only terminates the wrapper, leaving pnpm children running.
|
|
@@ -691,7 +733,16 @@ export function createDesktopPluginRuntime(service, activeProfileDir, invokingDi
|
|
|
691
733
|
const abort = new AbortController();
|
|
692
734
|
let handle;
|
|
693
735
|
try {
|
|
694
|
-
|
|
736
|
+
// `add` goes through Anywhere Labs' install boundary when that host
|
|
737
|
+
// publishes one, because their Desktop rejects `add` on `runPlugin`
|
|
738
|
+
// outright. Feature-detected, never assumed: this method is theirs
|
|
739
|
+
// alone, and on every other client — including the other desktop app
|
|
740
|
+
// in #292 — the ordinary call below is what runs, unchanged.
|
|
741
|
+
const boundary = prepared.args[0] === 'add' ? service.runExternalMarketPluginInstall : undefined;
|
|
742
|
+
const viaBoundary = boundary === undefined ? null : await exactNpmArgs(prepared.args);
|
|
743
|
+
handle = boundary === undefined || viaBoundary === null
|
|
744
|
+
? service.runPlugin(prepared.args, invokingDir, abort.signal)
|
|
745
|
+
: boundary.call(service, viaBoundary, invokingDir, abort.signal);
|
|
695
746
|
}
|
|
696
747
|
catch (error) {
|
|
697
748
|
const message = error instanceof Error ? error.message : String(error);
|
package/lib/sources.js
CHANGED
|
@@ -9,7 +9,7 @@ function validSubpath(subpath) {
|
|
|
9
9
|
return !subpath.split('/').some(seg => seg === '' || seg === '.' || seg === '..');
|
|
10
10
|
}
|
|
11
11
|
/** Registry tarball names must be plain npm package names, nothing fancier. */
|
|
12
|
-
const NPM_NAME_RE = /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
|
|
12
|
+
export const NPM_NAME_RE = /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
|
|
13
13
|
/**
|
|
14
14
|
* Parse a registry source url: a github repo, optionally with a
|
|
15
15
|
* `/tree/<branch>/<subpath>` suffix (how the curated list links monorepo
|
package/lib/types/dsh-cli.d.ts
CHANGED
|
@@ -154,17 +154,55 @@ export interface PluginCommandRuntime {
|
|
|
154
154
|
}>;
|
|
155
155
|
cancelActive(): boolean;
|
|
156
156
|
}
|
|
157
|
-
/**
|
|
157
|
+
/** One running package operation, however it was started. */
|
|
158
|
+
export interface DesktopPnpmHandleLike {
|
|
159
|
+
readonly stdout: NodeJS.ReadableStream;
|
|
160
|
+
readonly stderr: NodeJS.ReadableStream;
|
|
161
|
+
readonly done: Promise<{
|
|
162
|
+
readonly exitCode: number | null;
|
|
163
|
+
readonly signal: NodeJS.Signals | null;
|
|
164
|
+
}>;
|
|
165
|
+
cancel(): void;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Structural subset of DSH Desktop's public `desktopPnpm` contract.
|
|
169
|
+
*
|
|
170
|
+
* Anywhere Labs' DSH Desktop is ONE third-party client among several, and
|
|
171
|
+
* this interface exists only for it. Nothing here is part of the official
|
|
172
|
+
* DSH protocol — `desktopPnpm`, `installPlugin` and the install boundary
|
|
173
|
+
* below appear nowhere in `@deepseek-ai/*`. Every other client the market
|
|
174
|
+
* runs under, including other desktop apps, installs through the ordinary
|
|
175
|
+
* `dsh plugin --profile <p> add` CLI, and so does the market itself when
|
|
176
|
+
* none of these services are present.
|
|
177
|
+
*
|
|
178
|
+
* That is why every member past `runPlugin` is optional and reached by
|
|
179
|
+
* feature detection. A host that does not publish one simply never enters
|
|
180
|
+
* the branch, and the ordinary path it already used stays untouched — the
|
|
181
|
+
* cost of accommodating one vendor must not be paid by the others, or by
|
|
182
|
+
* the far larger number of people on plain `dsh web`.
|
|
183
|
+
*/
|
|
158
184
|
export interface DesktopPnpmLike {
|
|
159
|
-
runPlugin(args: readonly string[], invokingDir: string, signal?: AbortSignal):
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
185
|
+
runPlugin(args: readonly string[], invokingDir: string, signal?: AbortSignal): DesktopPnpmHandleLike;
|
|
186
|
+
/**
|
|
187
|
+
* Desktop 2.x refuses `add` through `runPlugin` — "plugin add must use the
|
|
188
|
+
* recoverable install boundary" (#215, #219, #272) — and offers this
|
|
189
|
+
* instead, which their launcher enables only for the selected market
|
|
190
|
+
* provider. Same arguments, same handle, no recovery receipt and no
|
|
191
|
+
* write-ahead log for the caller to reconcile.
|
|
192
|
+
*
|
|
193
|
+
* Optional because it is theirs: absent on every other host, including
|
|
194
|
+
* the other third-party desktop client in #292, which installs perfectly
|
|
195
|
+
* well through the ordinary CLI.
|
|
196
|
+
*
|
|
197
|
+
* Read from their published source rather than assumed: it accepts ONLY
|
|
198
|
+
* `add` with exactly one target of the form `name@exact.version`
|
|
199
|
+
* (`validateExternalMarketInstallArgs` in dsh-plugin-desktop/src/pnpm.ts).
|
|
200
|
+
* A `github:owner/repo` target is rejected before any process starts, so
|
|
201
|
+
* the 1085 catalog entries with no npm package — 57% of it — cannot be
|
|
202
|
+
* installed on that host by any spelling this market could send. That is
|
|
203
|
+
* a gap in their contract, not something to work around here.
|
|
204
|
+
*/
|
|
205
|
+
runExternalMarketPluginInstall?(args: readonly string[], invokingDir: string, signal?: AbortSignal): DesktopPnpmHandleLike;
|
|
168
206
|
}
|
|
169
207
|
/** Desktop runtime also owns cleanup of any operation started by this fiber. */
|
|
170
208
|
export interface DesktopPluginRuntime extends PluginCommandRuntime {
|
package/lib/types/sources.d.ts
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* Registry-source knowledge: how a curated registry entry's URL maps to an
|
|
3
3
|
* installable pnpm target. Pure string logic, no I/O.
|
|
4
4
|
*/
|
|
5
|
+
/** Registry tarball names must be plain npm package names, nothing fancier. */
|
|
6
|
+
export declare const NPM_NAME_RE: RegExp;
|
|
5
7
|
/**
|
|
6
8
|
* Parse a registry source url: a github repo, optionally with a
|
|
7
9
|
* `/tree/<branch>/<subpath>` suffix (how the curated list links monorepo
|
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.21.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/types/index.d.ts",
|
package/src/dsh-cli.ts
CHANGED
|
@@ -17,6 +17,8 @@ import { createProgressTracker, type ProgressPhase } from './ndjson.ts'
|
|
|
17
17
|
import { pluginArgsFor } from './pnpm-compat.ts'
|
|
18
18
|
import { isDshProfileName, profileDir } from './profile.ts'
|
|
19
19
|
import { activeRegion, DEFAULT_NPM_REGISTRY, routesFor, type Region } from './regions.ts'
|
|
20
|
+
import { NPM_NAME_RE } from './sources.ts'
|
|
21
|
+
import { fetchNpmLatest } from './updates.ts'
|
|
20
22
|
|
|
21
23
|
// 15 min default (slow networks + git installs), overridable for CI/tests.
|
|
22
24
|
// (#6 by @qichuang321.)
|
|
@@ -331,21 +333,102 @@ export interface PluginCommandRuntime {
|
|
|
331
333
|
cancelActive(): boolean
|
|
332
334
|
}
|
|
333
335
|
|
|
334
|
-
/**
|
|
336
|
+
/** One running package operation, however it was started. */
|
|
337
|
+
export interface DesktopPnpmHandleLike {
|
|
338
|
+
readonly stdout: NodeJS.ReadableStream
|
|
339
|
+
readonly stderr: NodeJS.ReadableStream
|
|
340
|
+
readonly done: Promise<{
|
|
341
|
+
readonly exitCode: number | null
|
|
342
|
+
readonly signal: NodeJS.Signals | null
|
|
343
|
+
}>
|
|
344
|
+
cancel(): void
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Structural subset of DSH Desktop's public `desktopPnpm` contract.
|
|
349
|
+
*
|
|
350
|
+
* Anywhere Labs' DSH Desktop is ONE third-party client among several, and
|
|
351
|
+
* this interface exists only for it. Nothing here is part of the official
|
|
352
|
+
* DSH protocol — `desktopPnpm`, `installPlugin` and the install boundary
|
|
353
|
+
* below appear nowhere in `@deepseek-ai/*`. Every other client the market
|
|
354
|
+
* runs under, including other desktop apps, installs through the ordinary
|
|
355
|
+
* `dsh plugin --profile <p> add` CLI, and so does the market itself when
|
|
356
|
+
* none of these services are present.
|
|
357
|
+
*
|
|
358
|
+
* That is why every member past `runPlugin` is optional and reached by
|
|
359
|
+
* feature detection. A host that does not publish one simply never enters
|
|
360
|
+
* the branch, and the ordinary path it already used stays untouched — the
|
|
361
|
+
* cost of accommodating one vendor must not be paid by the others, or by
|
|
362
|
+
* the far larger number of people on plain `dsh web`.
|
|
363
|
+
*/
|
|
335
364
|
export interface DesktopPnpmLike {
|
|
336
365
|
runPlugin(
|
|
337
366
|
args: readonly string[],
|
|
338
367
|
invokingDir: string,
|
|
339
368
|
signal?: AbortSignal,
|
|
340
|
-
):
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
369
|
+
): DesktopPnpmHandleLike
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Desktop 2.x refuses `add` through `runPlugin` — "plugin add must use the
|
|
373
|
+
* recoverable install boundary" (#215, #219, #272) — and offers this
|
|
374
|
+
* instead, which their launcher enables only for the selected market
|
|
375
|
+
* provider. Same arguments, same handle, no recovery receipt and no
|
|
376
|
+
* write-ahead log for the caller to reconcile.
|
|
377
|
+
*
|
|
378
|
+
* Optional because it is theirs: absent on every other host, including
|
|
379
|
+
* the other third-party desktop client in #292, which installs perfectly
|
|
380
|
+
* well through the ordinary CLI.
|
|
381
|
+
*
|
|
382
|
+
* Read from their published source rather than assumed: it accepts ONLY
|
|
383
|
+
* `add` with exactly one target of the form `name@exact.version`
|
|
384
|
+
* (`validateExternalMarketInstallArgs` in dsh-plugin-desktop/src/pnpm.ts).
|
|
385
|
+
* A `github:owner/repo` target is rejected before any process starts, so
|
|
386
|
+
* the 1085 catalog entries with no npm package — 57% of it — cannot be
|
|
387
|
+
* installed on that host by any spelling this market could send. That is
|
|
388
|
+
* a gap in their contract, not something to work around here.
|
|
389
|
+
*/
|
|
390
|
+
runExternalMarketPluginInstall?(
|
|
391
|
+
args: readonly string[],
|
|
392
|
+
invokingDir: string,
|
|
393
|
+
signal?: AbortSignal,
|
|
394
|
+
): DesktopPnpmHandleLike
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/** An npm name with a fully pinned version — the only target their boundary takes. */
|
|
398
|
+
const EXACT_NPM_TARGET_RE = /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*@\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Rewrite an `add` argv into the shape Anywhere Labs' install boundary
|
|
402
|
+
* accepts, or null when it cannot be expressed there.
|
|
403
|
+
*
|
|
404
|
+
* Their validator wants exactly one target of the form `name@1.2.3` — not a
|
|
405
|
+
* bare name, not `@latest`, and not a `github:` source (read from
|
|
406
|
+
* `validateExternalMarketInstallArgs`, dsh-plugin-desktop/src/pnpm.ts). The
|
|
407
|
+
* market sends a bare name for a registry plugin and `dshmarket@latest` for
|
|
408
|
+
* itself, so both need the version resolved before that boundary will take
|
|
409
|
+
* them.
|
|
410
|
+
*
|
|
411
|
+
* Returning null is a normal outcome, not a failure: a github-sourced plugin
|
|
412
|
+
* has no `name@version` spelling at all. The caller falls back to the
|
|
413
|
+
* ordinary path, which on that host reports their own refusal — an accurate
|
|
414
|
+
* message about their contract, rather than one this package invented.
|
|
415
|
+
*/
|
|
416
|
+
async function exactNpmArgs(args: readonly string[]): Promise<string[] | null> {
|
|
417
|
+
const targets = args.slice(1).filter(argument => !argument.startsWith('-'))
|
|
418
|
+
const target = targets[0]
|
|
419
|
+
if (targets.length !== 1 || target === undefined) return null
|
|
420
|
+
if (EXACT_NPM_TARGET_RE.test(target)) return [...args]
|
|
421
|
+
// A bare name, or one pinned to a dist-tag. Only a registry package can be
|
|
422
|
+
// resolved; `github:owner/repo` and file paths stop here.
|
|
423
|
+
const at = target.lastIndexOf('@')
|
|
424
|
+
const name = at > 0 ? target.slice(0, at) : target
|
|
425
|
+
if (!NPM_NAME_RE.test(name)) return null
|
|
426
|
+
const version = await fetchNpmLatest(name)
|
|
427
|
+
if (version === null) return null
|
|
428
|
+
const rewritten = `${name}@${version}`
|
|
429
|
+
if (!EXACT_NPM_TARGET_RE.test(rewritten)) return null
|
|
430
|
+
logEvent('info', 'install', `desktop install boundary needs an exact version: ${target} -> ${rewritten}`)
|
|
431
|
+
return args.map(argument => (argument === target ? rewritten : argument))
|
|
349
432
|
}
|
|
350
433
|
|
|
351
434
|
/** Desktop runtime also owns cleanup of any operation started by this fiber. */
|
|
@@ -799,9 +882,18 @@ export function createDesktopPluginRuntime(
|
|
|
799
882
|
}
|
|
800
883
|
|
|
801
884
|
const abort = new AbortController()
|
|
802
|
-
let handle:
|
|
885
|
+
let handle: DesktopPnpmHandleLike
|
|
803
886
|
try {
|
|
804
|
-
|
|
887
|
+
// `add` goes through Anywhere Labs' install boundary when that host
|
|
888
|
+
// publishes one, because their Desktop rejects `add` on `runPlugin`
|
|
889
|
+
// outright. Feature-detected, never assumed: this method is theirs
|
|
890
|
+
// alone, and on every other client — including the other desktop app
|
|
891
|
+
// in #292 — the ordinary call below is what runs, unchanged.
|
|
892
|
+
const boundary = prepared.args[0] === 'add' ? service.runExternalMarketPluginInstall : undefined
|
|
893
|
+
const viaBoundary = boundary === undefined ? null : await exactNpmArgs(prepared.args)
|
|
894
|
+
handle = boundary === undefined || viaBoundary === null
|
|
895
|
+
? service.runPlugin(prepared.args, invokingDir, abort.signal)
|
|
896
|
+
: boundary.call(service, viaBoundary, invokingDir, abort.signal)
|
|
805
897
|
} catch (error) {
|
|
806
898
|
const message = error instanceof Error ? error.message : String(error)
|
|
807
899
|
const busy = /another desktop pnpm operation is already running/i.test(message)
|
package/src/sources.ts
CHANGED
|
@@ -11,7 +11,7 @@ function validSubpath(subpath: string): boolean {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
/** Registry tarball names must be plain npm package names, nothing fancier. */
|
|
14
|
-
const NPM_NAME_RE = /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/
|
|
14
|
+
export const NPM_NAME_RE = /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* Parse a registry source url: a github repo, optionally with a
|