dshmarket 1.2.1 → 1.2.3
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/README.md +8 -0
- package/README.zh.md +8 -0
- package/client/client.js +141 -83
- package/client/client.js.map +1 -1
- package/lib/dsh-cli.js +164 -0
- package/lib/http.js +39 -0
- package/lib/install.js +110 -0
- package/lib/pnpm-compat.js +72 -0
- package/lib/profile.js +121 -0
- package/lib/routes.js +63 -552
- package/lib/sources.js +85 -0
- package/lib/themes.js +102 -0
- package/lib/types/dsh-cli.d.ts +61 -0
- package/lib/types/http.d.ts +12 -0
- package/lib/types/install.d.ts +49 -0
- package/lib/types/pnpm-compat.d.ts +42 -0
- package/lib/types/profile.d.ts +24 -0
- package/lib/types/routes.d.ts +7 -21
- package/lib/types/sources.d.ts +42 -0
- package/lib/types/themes.d.ts +40 -0
- package/lib/types/updates.d.ts +16 -0
- package/lib/updates.js +61 -0
- package/package.json +5 -2
- package/src/client/Market.module.css +4 -1
- package/src/client/MarketSection.tsx +25 -39
- package/src/client/locales.ts +2 -0
- package/src/client/market-data.ts +103 -7
- package/src/dsh-cli.ts +194 -0
- package/src/http.ts +41 -0
- package/src/install.ts +121 -0
- package/src/pnpm-compat.ts +83 -0
- package/src/profile.ts +120 -0
- package/src/routes.ts +68 -583
- package/src/sources.ts +84 -0
- package/src/themes.ts +120 -0
- package/src/updates.ts +70 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pnpm compatibility layer — everything the market needs to know about how
|
|
3
|
+
* different pnpm majors behave inside a dsh profile directory, kept pure and
|
|
4
|
+
* separately testable (test/unit + test/integration exercise this module
|
|
5
|
+
* against real pnpm 9/10/11).
|
|
6
|
+
*
|
|
7
|
+
* Verified behavior matrix (2026-08, pnpm 9.15.9 / 10.28.2 / 11.21.0):
|
|
8
|
+
* - workspace root, `add` without -w: pnpm 9 fails ERR_PNPM_ADDING_TO_ROOT;
|
|
9
|
+
* pnpm 10/11 succeed.
|
|
10
|
+
* - `add -w` where NO pnpm-workspace.yaml exists: ALL majors fail with
|
|
11
|
+
* "--workspace-root may only be used inside a workspace".
|
|
12
|
+
* - modules dir built by pnpm 9, then pnpm 10/11 mutate it:
|
|
13
|
+
* ERR_PNPM_PUBLIC_HOIST_PATTERN_DIFF (defaults drifted between majors).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { existsSync } from 'node:fs'
|
|
17
|
+
import { join } from 'node:path'
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Decide the argv for a `dsh plugin <add|remove> …` call in the given profile.
|
|
22
|
+
*
|
|
23
|
+
* pnpm 9 refuses to add at a workspace root without -w (#17, #20); every
|
|
24
|
+
* pnpm major refuses -w when the directory is NOT a workspace. So the flag
|
|
25
|
+
* is injected exactly when the profile has a pnpm-workspace.yaml.
|
|
26
|
+
* @param profileDir - resolved profile directory (owns pnpm-workspace.yaml, or not).
|
|
27
|
+
* @param pluginArgs - the raw args, e.g. ['add', 'dshmarket@latest'].
|
|
28
|
+
* @returns args with -w injected when — and only when — the profile is a workspace root.
|
|
29
|
+
*/
|
|
30
|
+
export function pluginArgsFor(profileDir: string, pluginArgs: string[]): string[] {
|
|
31
|
+
if (pluginArgs[0] !== 'add' && pluginArgs[0] !== 'remove') return pluginArgs
|
|
32
|
+
if (!existsSync(join(profileDir, 'pnpm-workspace.yaml'))) return pluginArgs
|
|
33
|
+
return [pluginArgs[0], '-w', ...pluginArgs.slice(1)]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** One recognized pnpm failure, with a bilingual explanation for the UI. */
|
|
37
|
+
export interface PnpmFailure {
|
|
38
|
+
code: 'adding-to-root' | 'not-a-workspace' | 'hoist-pattern-diff' | 'pnpm-missing'
|
|
39
|
+
/** Bilingual, actionable message shown to the user instead of the raw wall of text. */
|
|
40
|
+
message: string
|
|
41
|
+
/** True when re-running `pnpm install` in the profile is the documented recovery. */
|
|
42
|
+
recoverable: boolean
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Map a failed pnpm run's combined output to a known failure mode.
|
|
47
|
+
*
|
|
48
|
+
* dsh's own wrapper line ("dsh: pnpm failed in profile directory …") names no
|
|
49
|
+
* cause, so the market must recognize pnpm's real diagnostics itself (#20).
|
|
50
|
+
* @param output - stdout+stderr of the failed run.
|
|
51
|
+
* @returns the classified failure, or null when unrecognized (raw output is then shown as-is).
|
|
52
|
+
*/
|
|
53
|
+
export function classifyPnpmFailure(output: string): PnpmFailure | null {
|
|
54
|
+
if (output.includes('ERR_PNPM_PUBLIC_HOIST_PATTERN_DIFF')) {
|
|
55
|
+
return {
|
|
56
|
+
code: 'hoist-pattern-diff',
|
|
57
|
+
recoverable: true,
|
|
58
|
+
message: 'profile 的 node_modules 是旧版 pnpm 创建的,与当前 pnpm 的默认配置不兼容,需要重建后重试 / this profile\'s node_modules was created by a different pnpm major; it must be rebuilt (pnpm install) before changes can be applied',
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (output.includes('ERR_PNPM_ADDING_TO_ROOT')) {
|
|
62
|
+
return {
|
|
63
|
+
code: 'adding-to-root',
|
|
64
|
+
recoverable: false,
|
|
65
|
+
message: 'pnpm 拒绝在 workspace 根目录安装(缺少 -w)。这是市场的 bug,请升级 dshmarket 到最新版 / pnpm refused to add at a workspace root (missing -w); this is a market bug — please update dshmarket',
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (/--workspace-root may only be used inside a workspace/i.test(output)) {
|
|
69
|
+
return {
|
|
70
|
+
code: 'not-a-workspace',
|
|
71
|
+
recoverable: false,
|
|
72
|
+
message: 'profile 目录不是 pnpm workspace,却传入了 -w。这是市场的 bug,请升级 dshmarket 到最新版 / -w was passed but the profile is not a pnpm workspace; this is a market bug — please update dshmarket',
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (output.includes('pnpm not found on PATH')) {
|
|
76
|
+
return {
|
|
77
|
+
code: 'pnpm-missing',
|
|
78
|
+
recoverable: false,
|
|
79
|
+
message: '找不到 pnpm,请先在市场页顶部一键安装组件 / pnpm is not on PATH — use the one-click setup at the top of the market page',
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return null
|
|
83
|
+
}
|
package/src/profile.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Profile filesystem reads — everything the market learns from a dsh
|
|
3
|
+
* profile directory (manifest, lockfile, installed package trees). Pure
|
|
4
|
+
* functions of the directory contents; no processes, no network.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
|
8
|
+
import { homedir } from 'node:os'
|
|
9
|
+
import { join } from 'node:path'
|
|
10
|
+
|
|
11
|
+
/** Resolve a profile name to its directory under DSH_HOME (default ~/.dsh). */
|
|
12
|
+
export function profileDir(profile: string): string {
|
|
13
|
+
const home = process.env.DSH_HOME ?? join(homedir(), '.dsh')
|
|
14
|
+
return join(home, 'profiles', profile)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Community dependencies of the profile (official in-box scope filtered out). */
|
|
18
|
+
export function readInstalled(profile: string): Record<string, string> {
|
|
19
|
+
try {
|
|
20
|
+
const manifest = JSON.parse(readFileSync(join(profileDir(profile), 'package.json'), 'utf8')) as {
|
|
21
|
+
dependencies?: Record<string, string>
|
|
22
|
+
}
|
|
23
|
+
const installed: Record<string, string> = {}
|
|
24
|
+
for (const [name, spec] of Object.entries(manifest.dependencies ?? {})) {
|
|
25
|
+
if (!name.startsWith('@deepseek-ai/')) installed[name] = spec
|
|
26
|
+
}
|
|
27
|
+
return installed
|
|
28
|
+
} catch {
|
|
29
|
+
return {}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The version actually present in the profile's node_modules, or null. */
|
|
34
|
+
export function readInstalledVersion(profile: string, name: string): string | null {
|
|
35
|
+
try {
|
|
36
|
+
const manifest = JSON.parse(
|
|
37
|
+
readFileSync(join(profileDir(profile), 'node_modules', name, 'package.json'), 'utf8'),
|
|
38
|
+
) as { version?: string }
|
|
39
|
+
return manifest.version ?? null
|
|
40
|
+
} catch {
|
|
41
|
+
return null
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Pinned commit per `owner/repo` from the profile lockfile's codeload tarball URLs. */
|
|
46
|
+
export function readLockCommits(profile: string): Map<string, string> {
|
|
47
|
+
const commits = new Map<string, string>()
|
|
48
|
+
try {
|
|
49
|
+
const lock = readFileSync(join(profileDir(profile), 'pnpm-lock.yaml'), 'utf8')
|
|
50
|
+
for (const m of lock.matchAll(/codeload\.github\.com\/([^/\s]+\/[^/\s]+)\/tar\.gz\/([0-9a-f]{40})/g)) {
|
|
51
|
+
commits.set(m[1].toLowerCase(), m[2])
|
|
52
|
+
}
|
|
53
|
+
} catch { /* no lockfile — no git installs to report */ }
|
|
54
|
+
return commits
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** True when the installed package's manifest declares a dsh plugin surface. */
|
|
58
|
+
export function hasDshManifest(dir: string): boolean {
|
|
59
|
+
try {
|
|
60
|
+
const manifest = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')) as { dsh?: unknown }
|
|
61
|
+
return manifest.dsh !== undefined
|
|
62
|
+
} catch {
|
|
63
|
+
return false
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* True when the package's declared entry artifact actually exists — github
|
|
69
|
+
* source checkouts of build-required plugins ship no lib/, and promoting one
|
|
70
|
+
* into the bundle layer bricks the next boot (ERR_MODULE_NOT_FOUND kills the
|
|
71
|
+
* whole profile, #18).
|
|
72
|
+
*/
|
|
73
|
+
export function entryArtifactExists(dir: string): boolean {
|
|
74
|
+
try {
|
|
75
|
+
const manifest = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')) as {
|
|
76
|
+
main?: string
|
|
77
|
+
exports?: Record<string, unknown> | string
|
|
78
|
+
}
|
|
79
|
+
const candidates: string[] = []
|
|
80
|
+
if (typeof manifest.main === 'string') candidates.push(manifest.main)
|
|
81
|
+
const rootExport = typeof manifest.exports === 'string'
|
|
82
|
+
? manifest.exports
|
|
83
|
+
: (manifest.exports as Record<string, unknown> | undefined)?.['.']
|
|
84
|
+
if (typeof rootExport === 'string') candidates.push(rootExport)
|
|
85
|
+
else if (rootExport !== null && typeof rootExport === 'object') {
|
|
86
|
+
for (const value of Object.values(rootExport)) if (typeof value === 'string') candidates.push(value)
|
|
87
|
+
}
|
|
88
|
+
if (candidates.length === 0) candidates.push('index.js')
|
|
89
|
+
return candidates.some(rel => existsSync(join(dir, rel)))
|
|
90
|
+
} catch {
|
|
91
|
+
return false
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Plugin subdirectories (depth 2) of a collection checkout, as relative paths. */
|
|
96
|
+
export function pluginSubdirs(root: string): string[] {
|
|
97
|
+
const found: string[] = []
|
|
98
|
+
let level1: string[] = []
|
|
99
|
+
try {
|
|
100
|
+
level1 = readdirSync(root, { withFileTypes: true })
|
|
101
|
+
.filter(dirent => dirent.isDirectory() && /^[A-Za-z0-9_.-]+$/.test(dirent.name) && dirent.name !== 'node_modules')
|
|
102
|
+
.map(dirent => dirent.name)
|
|
103
|
+
} catch {
|
|
104
|
+
return found
|
|
105
|
+
}
|
|
106
|
+
for (const sub of level1) {
|
|
107
|
+
if (hasDshManifest(join(root, sub))) {
|
|
108
|
+
found.push(sub)
|
|
109
|
+
continue
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
for (const inner of readdirSync(join(root, sub), { withFileTypes: true })) {
|
|
113
|
+
if (!inner.isDirectory() || !/^[A-Za-z0-9_.-]+$/.test(inner.name) || inner.name === 'node_modules') continue
|
|
114
|
+
if (hasDshManifest(join(root, sub, inner.name))) found.push(`${sub}/${inner.name}`)
|
|
115
|
+
}
|
|
116
|
+
} catch { /* unreadable level — skip */ }
|
|
117
|
+
if (found.length >= 8) break
|
|
118
|
+
}
|
|
119
|
+
return found.slice(0, 8)
|
|
120
|
+
}
|