dshmarket 1.20.3 → 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 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
  /**
@@ -185,7 +187,10 @@ export function toolSearchDirs(platform = process.platform, env = process.env, h
185
187
  dirs.push(join(home, 'Library', 'pnpm'), join(home, '.local', 'share', 'pnpm'));
186
188
  }
187
189
  dirs.push(nodeBinDir, ...extraPathDirs);
188
- return dirs;
190
+ // Deduped: PNPM_HOME usually names one of the defaults below it, and a
191
+ // list that says the same directory twice reads as carelessness in the
192
+ // one place a user goes looking for an answer.
193
+ return [...new Set(dirs.filter(dir => dir.trim() !== ''))];
189
194
  }
190
195
  function spawnEnv() {
191
196
  // pnpm v10+ blocks forever on a silent interactive prompt without a TTY;
@@ -276,6 +281,46 @@ export function dshArgv() {
276
281
  // Bare `dsh` is a .cmd shim on Windows that only a shell can start (#13).
277
282
  return { file: 'dsh', args: [], cwd: undefined, viaShell: winCmdShim };
278
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
+ }
279
324
  /**
280
325
  * Kill a spawned child and, on Windows, its whole process tree — `kill()`
281
326
  * there only terminates the wrapper, leaving pnpm children running.
@@ -478,7 +523,18 @@ export function provisionHint(corepackOutput, npmOutput, npmFound = true) {
478
523
  if (/ETIMEDOUT|ECONNREFUSED|ENOTFOUND|EAI_AGAIN|network|proxy|certificate/i.test(`${corepackOutput}\n${npmOutput}`)) {
479
524
  return '装 pnpm 时网络失败。若你在受限网络下,corepack 的 shim 也下载不到 pnpm 本体——请改用完整安装或指定镜像:brew install pnpm(macOS/Linux),或 npm i -g pnpm --registry <你的镜像> / Network failure while installing pnpm. On a restricted network the corepack shim cannot download pnpm either — install it fully or point at a mirror: `brew install pnpm`, or `npm i -g pnpm --registry <your mirror>`';
480
525
  }
481
- return undefined;
526
+ // Everything reported success and pnpm still will not run (#228 by
527
+ // @ZhengXin1023: corepack exit=0, npm -g exit=0, npm found — and the
528
+ // install button stayed locked with nothing said).
529
+ //
530
+ // This used to return undefined, which left the case that most needs an
531
+ // explanation with none: the user is told "setup failed" while every step
532
+ // they can see succeeded, and their complaint was exactly that — "又不告诉
533
+ // 我怎么手动配置". Whatever the cause, the actionable question is the same
534
+ // one, so ask it: where is pnpm, and is that anywhere this process looks?
535
+ const searched = toolSearchDirs().join(process.platform === 'win32' ? ' ; ' : ' : ');
536
+ const locate = process.platform === 'win32' ? 'where pnpm' : 'which pnpm';
537
+ return `pnpm 装好了,但这个 dsh 进程仍然启动不了它——安装步骤都成功,只是装到的位置不在它搜索的范围内。已找过:${searched}。请在终端执行 \`${locate}\` 看 pnpm 实际在哪:如果它不在上面这些目录里,把该目录设为 PNPM_HOME 后重启 dsh(\`export PNPM_HOME=<那个目录>\`),或者干脆从一个能直接运行 pnpm 的终端里启动 dsh。注意必须重启——正在运行的进程读不到新设的环境变量 / pnpm is installed but this dsh process still cannot start it: every step succeeded, the binary just landed somewhere this process does not look. Searched: ${searched}. Run \`${locate}\` in a terminal to see where pnpm actually is; if that directory is not in the list above, set PNPM_HOME to it and restart dsh (\`export PNPM_HOME=<that directory>\`), or simply start dsh from a terminal where \`pnpm\` already runs. The restart matters — a running process cannot see a newly set variable`;
482
538
  }
483
539
  /** Singleton progress state; the status route reads it, runDshPlugin writes it. */
484
540
  export const progress = {
@@ -677,7 +733,16 @@ export function createDesktopPluginRuntime(service, activeProfileDir, invokingDi
677
733
  const abort = new AbortController();
678
734
  let handle;
679
735
  try {
680
- handle = service.runPlugin(prepared.args, invokingDir, abort.signal);
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);
681
746
  }
682
747
  catch (error) {
683
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
@@ -154,17 +154,55 @@ export interface PluginCommandRuntime {
154
154
  }>;
155
155
  cancelActive(): boolean;
156
156
  }
157
- /** Structural subset of DSH Desktop's public `desktopPnpm` contract. */
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
- readonly stdout: NodeJS.ReadableStream;
161
- readonly stderr: NodeJS.ReadableStream;
162
- readonly done: Promise<{
163
- readonly exitCode: number | null;
164
- readonly signal: NodeJS.Signals | null;
165
- }>;
166
- cancel(): void;
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 {
@@ -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.20.3",
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.)
@@ -186,7 +188,10 @@ export function toolSearchDirs(
186
188
  dirs.push(join(home, 'Library', 'pnpm'), join(home, '.local', 'share', 'pnpm'))
187
189
  }
188
190
  dirs.push(nodeBinDir, ...extraPathDirs)
189
- return dirs
191
+ // Deduped: PNPM_HOME usually names one of the defaults below it, and a
192
+ // list that says the same directory twice reads as carelessness in the
193
+ // one place a user goes looking for an answer.
194
+ return [...new Set(dirs.filter(dir => dir.trim() !== ''))]
190
195
  }
191
196
 
192
197
  function spawnEnv(): NodeJS.ProcessEnv {
@@ -328,21 +333,102 @@ export interface PluginCommandRuntime {
328
333
  cancelActive(): boolean
329
334
  }
330
335
 
331
- /** Structural subset of DSH Desktop's public `desktopPnpm` contract. */
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
+ */
332
364
  export interface DesktopPnpmLike {
333
365
  runPlugin(
334
366
  args: readonly string[],
335
367
  invokingDir: string,
336
368
  signal?: AbortSignal,
337
- ): {
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
- }
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))
346
432
  }
347
433
 
348
434
  /** Desktop runtime also owns cleanup of any operation started by this fiber. */
@@ -553,7 +639,18 @@ export function provisionHint(corepackOutput: string, npmOutput: string, npmFoun
553
639
  if (/ETIMEDOUT|ECONNREFUSED|ENOTFOUND|EAI_AGAIN|network|proxy|certificate/i.test(`${corepackOutput}\n${npmOutput}`)) {
554
640
  return '装 pnpm 时网络失败。若你在受限网络下,corepack 的 shim 也下载不到 pnpm 本体——请改用完整安装或指定镜像:brew install pnpm(macOS/Linux),或 npm i -g pnpm --registry <你的镜像> / Network failure while installing pnpm. On a restricted network the corepack shim cannot download pnpm either — install it fully or point at a mirror: `brew install pnpm`, or `npm i -g pnpm --registry <your mirror>`'
555
641
  }
556
- return undefined
642
+ // Everything reported success and pnpm still will not run (#228 by
643
+ // @ZhengXin1023: corepack exit=0, npm -g exit=0, npm found — and the
644
+ // install button stayed locked with nothing said).
645
+ //
646
+ // This used to return undefined, which left the case that most needs an
647
+ // explanation with none: the user is told "setup failed" while every step
648
+ // they can see succeeded, and their complaint was exactly that — "又不告诉
649
+ // 我怎么手动配置". Whatever the cause, the actionable question is the same
650
+ // one, so ask it: where is pnpm, and is that anywhere this process looks?
651
+ const searched = toolSearchDirs().join(process.platform === 'win32' ? ' ; ' : ' : ')
652
+ const locate = process.platform === 'win32' ? 'where pnpm' : 'which pnpm'
653
+ return `pnpm 装好了,但这个 dsh 进程仍然启动不了它——安装步骤都成功,只是装到的位置不在它搜索的范围内。已找过:${searched}。请在终端执行 \`${locate}\` 看 pnpm 实际在哪:如果它不在上面这些目录里,把该目录设为 PNPM_HOME 后重启 dsh(\`export PNPM_HOME=<那个目录>\`),或者干脆从一个能直接运行 pnpm 的终端里启动 dsh。注意必须重启——正在运行的进程读不到新设的环境变量 / pnpm is installed but this dsh process still cannot start it: every step succeeded, the binary just landed somewhere this process does not look. Searched: ${searched}. Run \`${locate}\` in a terminal to see where pnpm actually is; if that directory is not in the list above, set PNPM_HOME to it and restart dsh (\`export PNPM_HOME=<that directory>\`), or simply start dsh from a terminal where \`pnpm\` already runs. The restart matters — a running process cannot see a newly set variable`
557
654
  }
558
655
 
559
656
  /** Live progress of the running plugin command, for the status route. */
@@ -785,9 +882,18 @@ export function createDesktopPluginRuntime(
785
882
  }
786
883
 
787
884
  const abort = new AbortController()
788
- let handle: ReturnType<DesktopPnpmLike['runPlugin']>
885
+ let handle: DesktopPnpmHandleLike
789
886
  try {
790
- handle = service.runPlugin(prepared.args, invokingDir, abort.signal)
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)
791
897
  } catch (error) {
792
898
  const message = error instanceof Error ? error.message : String(error)
793
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