Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
866 changes: 458 additions & 408 deletions client/client.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion client/client.js.map

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions lib/http/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -1011,8 +1011,8 @@ export function mountPluginHubRoutes(webServer, profile, loader) {
else {
attempts.push(`package name conflict: \`${gitRepo}\` declares npm name \`${declared}\`, which is already present in the profile and cannot be matched to this repo (that npm name is likely registered by another repository)`);
const error = lang === 'zh'
? `already installed: ${declared} —— ${gitRepo} 声明的 npm 包名「${declared}」已存在于当前 profile,且无法匹配到本仓库(该包名在 npm 上可能被其他仓库占用)。请先移除现有的 ${declared}(dsh plugin remove ${declared}),再重新安装;或改装该包名对应的官方 npm 包。`
: `already installed: ${declared} — ${gitRepo} declares the npm package name "${declared}", which is already present in the profile and cannot be matched to this repo (that npm name is likely registered by another repository). Remove the existing ${declared} dependency (dsh plugin remove ${declared}) first, then retry; or install the official npm package under that name.`;
? `already installed: ${declared} —— ${gitRepo} 声明的 npm 包名「${declared}」已存在于当前 profile,且无法匹配到本仓库(该包名在 npm 上可能被其他仓库占用)。请先移除现有的 ${declared}(dsh plugin --profile ${profile} remove ${declared}),再重新安装;或改装该包名对应的官方 npm 包。`
: `already installed: ${declared} — ${gitRepo} declares the npm package name "${declared}", which is already present in the profile and cannot be matched to this repo (that npm name is likely registered by another repository). Remove the existing ${declared} dependency (dsh plugin --profile ${profile} remove ${declared}) first, then retry; or install the official npm package under that name.`;
sendJson(response, 409, { error, attempts });
return;
}
Expand Down
15 changes: 11 additions & 4 deletions src/client/components/PluginHubSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import type { MouseEvent } from 'react'
import styles from '../styles/Header.module.css'
import modalStyles from '../styles/Modal.module.css'
import type { EnvInfo, HubPlugin, InstallChannel, SectionProps, ToastState } from '../types.ts'
import { installCommandOf, repoFromInstallTarget } from '../logic/install-command.ts'
import { installCommandOf, repoFromInstallTarget, setActiveProfile, uninstallCommandOf } from '../logic/install-command.ts'
import { PLUGIN_VERSION } from '../logic/constants.ts'
import { ensurePluginCss } from '../logic/ensure-plugin-css.ts'
import { getEnv, revealInstallFolder } from '../data/host.ts'
Expand Down Expand Up @@ -126,7 +126,13 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
const [env, setEnv] = useState<EnvInfo | null>(null)
useEffect(() => {
let alive = true
void getEnv().then((info) => { if (alive) setEnv(info) })
void getEnv().then((info) => {
if (!alive) return
setEnv(info)
// 命令生成统一按宿主真实档位:目录下发的命令都写着 --profile web,
// 桌面端(desktop 档)照抄会装进 web 档(see retargetProfile)。
setActiveProfile(info?.profile)
})
return () => { alive = false }
}, [])

Expand Down Expand Up @@ -322,7 +328,7 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
const copyUninstallCommand = async () => {
const name = uninstallItem ? uninstallItem.name : (uninstallPlugin ? catalog.installedName(uninstallPlugin) : null)
if (!name) return
const ok = await doCopy(`dsh plugin remove ${name}`)
const ok = await doCopy(uninstallCommandOf(name, env?.profile))
if (ok) setToast({ id: Date.now(), kind: 'copied' })
}

Expand Down Expand Up @@ -495,7 +501,8 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
enableDsh: hubSettings.enableDshInstall,
onOpenSettings: () => setView('settings'),
// 当前 profile:一键插入的 Hub 更新命令用它拼 --profile
profile: env?.profile ?? 'web',
// 不回退 'web':未拿到 /env 时留空,命令就不带 --profile,好过指向错误档位
profile: env?.profile ?? '',
onInstallCustom: (raw, opts) => {
const target = raw.trim()
if (!target) return
Expand Down
15 changes: 10 additions & 5 deletions src/client/components/views/CustomInstallView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,13 @@ export function CustomInstallView({ t, onInstallCustom, enableNpm, enableGit, en
enableDsh: boolean
/** 引导去「设置 → 安全信任」打开被关闭的通道开关 */
onOpenSettings: () => void
/** 当前 dsh profile 名(宿主 /env 快照):一键插入的更新命令用它拼 --profile */
/** 当前 dsh profile 名(宿主 /env 快照):一键插入的更新命令用它拼 --profile。
* 未拿到前为空串(不回退 'web'):文案退化为占位符、插入按钮隐藏,
* 避免生成一条指向错误档位的命令。 */
profile: string
}) {
/** 文案展示用:档位未知时给占位符,而不是一个具体的(可能是错的)档位名 */
const profileLabel = profile === '' ? '<profile>' : profile
/** 自定义安装:NPM 包输入与格式错误('' = 无错误) */
const [npmQuery, setNpmQuery] = useState('')
const [npmError, setNpmError] = useState('')
Expand Down Expand Up @@ -176,7 +180,7 @@ export function CustomInstallView({ t, onInstallCustom, enableNpm, enableGit, en
const raw = cmdQuery.trim()
if (!raw) return
const parsed = parseDshCommand(raw)
if (parsed === null) { setCmdError(t('dshCmdInvalid', { profile })); return }
if (parsed === null) { setCmdError(t('dshCmdInvalid', { profile: profileLabel })); return }
// add / update 统一走安装流程:目标已安装时弹窗自动转「更新」覆盖重装(服务端 mode=update 放行),
// 未安装时即为普通安装 —— 粘贴 `dsh plugin --profile web update dsh-plugin` 这类命令即可更新到最新
onInstallCustom(parsed.target, { installChannel: 'dsh' })
Expand All @@ -185,6 +189,7 @@ export function CustomInstallView({ t, onInstallCustom, enableNpm, enableGit, en
}
/** 一键插入 DSH Plugin Hub 自身的更新命令:按当前 profile 拼官方命令填进输入框,用户确认后提交 */
const insertHubUpdate = () => {
if (profile === '') return
setCmdQuery(`dsh plugin --profile ${profile} update dsh-plugin`)
setCmdError('')
}
Expand Down Expand Up @@ -292,7 +297,7 @@ export function CustomInstallView({ t, onInstallCustom, enableNpm, enableGit, en
}, h(HelpIcon)),
// 一键插入 Hub 自身的更新命令:按当前 profile 拼 `dsh plugin --profile <p> update dsh-plugin`
// 填入输入框(命令卡片被安全开关禁用时不显示,插入后也提交不了)
enableDsh
enableDsh && profile !== ''
? h('button', {
type: 'button',
className: styles.installInsertBtn,
Expand All @@ -303,7 +308,7 @@ export function CustomInstallView({ t, onInstallCustom, enableNpm, enableGit, en
!enableDsh
? channelOffRow
: [
h('div', { className: styles.installExample }, t('dshCmdExample', { profile })),
h('div', { className: styles.installExample }, t('dshCmdExample', { profile: profileLabel })),
h('div', { className: styles.installRow },
h('input', {
className: cmdError ? `${styles.installInput} ${styles.installInputError}` : styles.installInput,
Expand Down Expand Up @@ -346,7 +351,7 @@ export function CustomInstallView({ t, onInstallCustom, enableNpm, enableGit, en
h('div', { className: modal.modalBody },
// 格式清单:一行一条;`命令\t说明` 的说明跟在命令后,纯说明行以 \t 开头
h('div', { className: styles.helpBody },
t(HELP_CARDS[helpFor].contentKey, { profile }).split('\n').map((line, i) => {
t(HELP_CARDS[helpFor].contentKey, { profile: profileLabel }).split('\n').map((line, i) => {
const [cmd, note] = line.split('\t')
return h('div', { key: i, className: styles.helpLine },
cmd !== '' ? h('span', { className: styles.helpCmd }, cmd) : null,
Expand Down
74 changes: 63 additions & 11 deletions src/client/logic/install-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,52 @@
import type { HubPlugin } from '../types.ts'
import { githubReleaseTarget } from '../../server/services/install/release-target.ts'

/** The profile the catalog stamps onto every entry that is not tied to a specific
* one. It is a publishing placeholder, not a chosen target: measured against the
* live catalog, all 13448 entries carry `--profile web` and none names another
* profile. Copying such a command on any other host — DSH Desktop runs the
* `desktop` profile — installs into the wrong profile, so it is re-pointed at
* the one this host actually serves. */
const CATALOG_DEFAULT_PROFILE = 'web'

/** The profile token of a `dsh plugin` command (`--profile web` / `--profile=web`). */
const PROFILE_ARG_RE = /(\s--profile(?:=|\s+))(\S+)(?=\s|$)/

/** The profile this host serves, learned from `GET /dsh-plugin-hub/env`.
* Held at module scope so every command builder — including the ones called from
* plain render paths — reads the same value without threading it through props. */
let activeProfile = ''

/** Record the profile the host reports, so generated commands target it.
* @param profile - profile name from the /env snapshot; anything else clears it. */
export function setActiveProfile(profile: string | undefined): void {
activeProfile = typeof profile === 'string' ? profile.trim() : ''
}

/** The profile the host reports, or '' before `/env` has answered. */
export function getActiveProfile(): string {
return activeProfile
}

/**
* Re-point a catalog command at the profile this host serves.
*
* The catalog's command is authoritative for CLI-only plugins, which need their
* own profile (`--profile dsh-tui`) that cannot be inferred from the package name.
* Those name a profile other than {@link CATALOG_DEFAULT_PROFILE} and are kept
* verbatim; only the blanket placeholder is replaced, so a Desktop user copying
* it installs into `desktop` instead of `web`.
*
* @param command - a catalog command or any generated `dsh plugin ...` string.
* @param profile - profile to target; '' leaves the command untouched.
* @returns the command targeting `profile`, or the original when nothing applies.
*/
export function retargetProfile(command: string, profile: string): string {
if (command === '' || profile === '' || profile === CATALOG_DEFAULT_PROFILE) return command
return command.replace(PROFILE_ARG_RE, (match, prefix: string, name: string) =>
name === CATALOG_DEFAULT_PROFILE ? `${prefix}${profile}` : match)
}

/** Backend attempts are chronological; report the last executed command before a display fallback.
* These prefixes are the task queue's recorded command shapes, never commands to execute here.
*/
Expand Down Expand Up @@ -42,22 +88,28 @@ export function installTargetOf(p: HubPlugin): { target: string; via: 'npm' | 'r

/** 展示用安装命令(复制/弹窗):目录下发的权威命令优先(CLI-only 插件如 dsh-tui
* 需专属 profile(--profile dsh-tui),无法从 repo/npm 包名推断,必须用官方命令);
* 常规插件无目录命令时按通道回退生成:npm 显示包名,git 显示显式 HTTPS URL。 */
export function installCommandOf(p: HubPlugin, withProfile = false): string {
* 常规插件无目录命令时按通道回退生成:npm 显示包名,git 显示显式 HTTPS URL。
* 目录命令里的默认档位占位符会改写成宿主真实档位 —— 见 {@link retargetProfile};
* 插件自带的专属档位原样保留。
* @param profile - profile to target; defaults to the one `/env` reported.
*/
export function installCommandOf(p: HubPlugin, withProfile = false, profile = getActiveProfile()): string {
const { target, via } = installTargetOf(p)
if (via === 'npm') {
const cmd = p.install?.command
if (cmd) return cmd
} else {
// release 与 git 通道共用目录下发的 githubCommand(git+ 显式 HTTPS 或 release 直链)
const cmd = p.install?.githubCommand
if (cmd) return cmd
}
const prefix = `dsh plugin${withProfile ? ' --profile web' : ''} add `
const catalog = via === 'npm' ? p.install?.command : p.install?.githubCommand
if (catalog) return retargetProfile(catalog, profile)
const prefix = `dsh plugin${withProfile && profile !== '' ? ` --profile ${profile}` : ''} add `
if (via === 'npm' || via === 'release') return `${prefix}${target}`
return `${prefix}git+https://github.com/${target}.git`
}

/** 卸载命令(复制/弹窗):官方 CLI 要求 --profile,缺省会被直接拒绝
* (`error: required option '--profile <name>' not specified`),故带上宿主档位。
* @param profile - profile to target; defaults to the one `/env` reported.
*/
export function uninstallCommandOf(name: string, profile = getActiveProfile()): string {
return profile === '' ? `dsh plugin remove ${name}` : `dsh plugin --profile ${profile} remove ${name}`
}

/** Normalize a task/install target to its owner/repo display identity. */
export function repoFromInstallTarget(value: string): string {
const input = value.trim()
Expand Down
4 changes: 2 additions & 2 deletions src/server/http/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -997,8 +997,8 @@ export function mountPluginHubRoutes(webServer: WebServerService, profile: strin
} else {
attempts.push(`package name conflict: \`${gitRepo}\` declares npm name \`${declared}\`, which is already present in the profile and cannot be matched to this repo (that npm name is likely registered by another repository)`)
const error = lang === 'zh'
? `already installed: ${declared} —— ${gitRepo} 声明的 npm 包名「${declared}」已存在于当前 profile,且无法匹配到本仓库(该包名在 npm 上可能被其他仓库占用)。请先移除现有的 ${declared}(dsh plugin remove ${declared}),再重新安装;或改装该包名对应的官方 npm 包。`
: `already installed: ${declared} — ${gitRepo} declares the npm package name "${declared}", which is already present in the profile and cannot be matched to this repo (that npm name is likely registered by another repository). Remove the existing ${declared} dependency (dsh plugin remove ${declared}) first, then retry; or install the official npm package under that name.`
? `already installed: ${declared} —— ${gitRepo} 声明的 npm 包名「${declared}」已存在于当前 profile,且无法匹配到本仓库(该包名在 npm 上可能被其他仓库占用)。请先移除现有的 ${declared}(dsh plugin --profile ${profile} remove ${declared}),再重新安装;或改装该包名对应的官方 npm 包。`
: `already installed: ${declared} — ${gitRepo} declares the npm package name "${declared}", which is already present in the profile and cannot be matched to this repo (that npm name is likely registered by another repository). Remove the existing ${declared} dependency (dsh plugin --profile ${profile} remove ${declared}) first, then retry; or install the official npm package under that name.`
sendJson(response, 409, { error, attempts })
return
}
Expand Down
6 changes: 5 additions & 1 deletion tests/catalog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,11 @@ test('pluginIssueUrl: 标题点明错误原因,不再用 Install/Remove 动作
test('installCommandOf: GitHub source uses an explicit HTTPS URL', () => {
const p = normalize({ s: 'aegis', n: 'Aegis', r: 'ganyuanran/aegis' })
assert.equal(installCommandOf(p), 'dsh plugin add git+https://github.com/ganyuanran/aegis.git')
assert.equal(installCommandOf(p, true), 'dsh plugin --profile web add git+https://github.com/ganyuanran/aegis.git')
// 档位由调用方给出(不再是写死的 web):withProfile 只是「要不要带 --profile」的开关
assert.equal(
installCommandOf(p, true, 'web'),
'dsh plugin --profile web add git+https://github.com/ganyuanran/aegis.git',
)
})

test('repoFromInstallTarget: keeps catalog identity across Git target formats', () => {
Expand Down
Loading