Skip to content

Commit a23caac

Browse files
committed
feat: auto-detect counterpart plugins when installing from marketplace
When selecting plugins from the market, automatically find matching frontend/backend counterparts (by name ↔ name_ui convention), skip those already installed locally or already selected, and prompt the user to install them together. Made-with: Cursor
1 parent 9a1527f commit a23caac

3 files changed

Lines changed: 82 additions & 6 deletions

File tree

src/commands/plugin/add.ts

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
// plugin/add.ts — 插件添加
22
import * as clack from '@clack/prompts'
33
import chalk from 'chalk'
4-
import { basename } from 'path'
54
import { t } from '../../lib/i18n.js'
65
import { requireProjectDir, fatal } from '../../lib/errors.js'
7-
import { fetchPluginMarketData, filterByType, getMarketPluginType } from '../../lib/plugin-market.js'
8-
import { installFromMarket, installFrontendPlugin, installBackendPlugin, runPnpmInstall } from '../../lib/plugin-install.js'
6+
import { fetchPluginMarketData, filterByType, getMarketPluginType, findCounterparts } from '../../lib/plugin-market.js'
7+
import { installFromMarket, installFrontendPlugin, installBackendPlugin, runPnpmInstall, scanInstalledPlugins } from '../../lib/plugin-install.js'
98
import type { PluginData } from '../../types/plugin.js'
10-
import { inferPluginType, stripWebPluginSuffix } from '../../types/plugin.js'
9+
import { stripWebPluginSuffix } from '../../types/plugin.js'
1110

1211
/**
1312
* fba plugin add — 添加插件
@@ -114,10 +113,33 @@ export async function pluginMarketFlow(projectDir: string) {
114113
})
115114
if (clack.isCancel(selected) || selected.length === 0) return
116115

117-
const selectedPlugins = selected
116+
let selectedPlugins = selected
118117
.map(i => filtered[i as number])
119118
.filter((p): p is PluginData => p !== undefined)
120119

120+
// 配套插件检测
121+
const installed = scanInstalledPlugins(projectDir)
122+
const counterparts = findCounterparts(selectedPlugins, plugins, installed)
123+
124+
if (counterparts.length > 0) {
125+
const cpSelected = await clack.multiselect({
126+
message: `${t('pluginCounterpartFound')} ${chalk.dim(t('multiselectHint'))}`,
127+
options: counterparts.map((p, i) => ({
128+
value: i,
129+
label: `${p.plugin.summary}`,
130+
hint: `${t('pluginCounterpartHint')} | ${getMarketPluginType(p)} | @${p.plugin.author}`,
131+
})),
132+
initialValues: counterparts.map((_, i) => i),
133+
required: false,
134+
})
135+
if (!clack.isCancel(cpSelected) && cpSelected.length > 0) {
136+
const extras = cpSelected
137+
.map(i => counterparts[i as number])
138+
.filter((p): p is PluginData => p !== undefined)
139+
selectedPlugins = [...selectedPlugins, ...extras]
140+
}
141+
}
142+
121143
// 安装
122144
clack.log.step(t('pluginInstalling'))
123145
const installResult = await installFromMarket(projectDir, selectedPlugins)

src/lib/i18n.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ const messages = {
110110
pluginMarketTitle: "FBA 插件市场",
111111
pluginSearch: "搜索",
112112
pluginFilter: "筛选",
113+
pluginCounterpartFound: "以下插件有配套的前/后端插件,是否一并安装?",
114+
pluginCounterpartHint: "配套",
113115
pluginInstalling: "正在安装插件...",
114116
pluginInstallSuccess: "插件安装成功",
115117
pluginInstallFail: "插件安装失败",
@@ -208,8 +210,12 @@ const messages = {
208210

209211
// Go
210212
goEnteringProject: "进入项目:",
213+
goEnteringBackend: "进入后端目录:",
214+
goEnteringFrontend: "进入前端目录:",
211215
goShell: "Shell:",
212216
goExitHint: '输入 "exit" 或 Ctrl+D 返回',
217+
optGoServer: "进入后端 (server) 目录",
218+
optGoFrontend: "进入前端 (web) 目录",
213219

214220
// Remove
215221
removeSelectProjects: "选择要移除的项目",
@@ -381,6 +387,8 @@ const messages = {
381387
pluginMarketTitle: "FBA Plugin Marketplace",
382388
pluginSearch: "Search",
383389
pluginFilter: "Filter",
390+
pluginCounterpartFound: "The following plugins have matching frontend/backend counterparts. Install them too?",
391+
pluginCounterpartHint: "counterpart",
384392
pluginInstalling: "Installing plugins...",
385393
pluginInstallSuccess: "Plugin installed",
386394
pluginInstallFail: "Plugin installation failed",
@@ -474,8 +482,12 @@ const messages = {
474482
validOptions: "Valid options",
475483

476484
goEnteringProject: "Entering project:",
485+
goEnteringBackend: "Entering backend directory:",
486+
goEnteringFrontend: "Entering frontend directory:",
477487
goShell: "Shell:",
478488
goExitHint: 'Type "exit" or Ctrl+D to return',
489+
optGoServer: "Enter backend (server) directory",
490+
optGoFrontend: "Enter frontend (web) directory",
479491

480492
removeSelectProjects: "Select projects to remove",
481493
removeConfirm:

src/lib/plugin-market.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import { homedir } from 'os'
55
import { ofetch } from 'ofetch'
66
import { basename } from 'path'
77
import type { PluginData } from '../types/plugin.js'
8-
import { inferPluginType } from '../types/plugin.js'
8+
import { inferPluginType, PLUGIN_WEB_SUFFIX, stripWebPluginSuffix } from '../types/plugin.js'
9+
import type { InstalledPlugin } from '../types/plugin.js'
910

1011
const PLUGIN_DATA_URL =
1112
'https://raw.githubusercontent.com/fastapi-practices/plugins/refs/heads/master/plugins-data.ts'
@@ -74,3 +75,44 @@ export function filterByType(plugins: PluginData[], type: string): PluginData[]
7475
export function getMarketPluginType(p: PluginData): 'web' | 'server' {
7576
return inferPluginType(basename(p.git.path))
7677
}
78+
79+
/**
80+
* 在市场数据中查找已选插件的未选配套插件(前端 ↔ 后端)
81+
*
82+
* 排除:已被用户选中的、本地已安装的
83+
*/
84+
export function findCounterparts(
85+
selected: PluginData[],
86+
allPlugins: PluginData[],
87+
installedPlugins: InstalledPlugin[],
88+
): PluginData[] {
89+
const selectedPaths = new Set(selected.map(p => basename(p.git.path)))
90+
const installedNames = new Set(installedPlugins.map(p => p.name))
91+
92+
const counterparts: PluginData[] = []
93+
const seen = new Set<string>()
94+
95+
for (const sel of selected) {
96+
const name = basename(sel.git.path)
97+
const type = inferPluginType(name)
98+
99+
// server → 找 name_ui ; web → 找去掉 _ui
100+
const counterpartName = type === 'server'
101+
? `${name}${PLUGIN_WEB_SUFFIX}`
102+
: stripWebPluginSuffix(name)
103+
104+
if (selectedPaths.has(counterpartName)) continue
105+
if (installedNames.has(counterpartName)) continue
106+
// 前端插件本地目录名是去掉 _ui 的
107+
if (type === 'server' && installedNames.has(stripWebPluginSuffix(counterpartName))) continue
108+
if (seen.has(counterpartName)) continue
109+
110+
const match = allPlugins.find(p => basename(p.git.path) === counterpartName)
111+
if (match) {
112+
counterparts.push(match)
113+
seen.add(counterpartName)
114+
}
115+
}
116+
117+
return counterparts
118+
}

0 commit comments

Comments
 (0)