Files
CherryHQ-cherry-studio/scripts/before-pack.js
SuYao 6b9daa749e feat: add plugin package installation from ZIP,directory, remote (#12426)
* feat: add plugin install

* fix: some bug

* fix: i18n

* refactor: rename 'active-directory' to 'resource'

* feat(plugin): support remote fetch plugin

* refactor: improve error display

* feat: add cache protocol

* fix(plugin): address code review issues for PR #12426

- Fix command injection vulnerability by adding -- separator before
  positional arguments in git clone and ls-remote commands
- Extract duplicate file helpers (directoryExists, fileExists, pathExists)
  to shared @main/utils/file utility
- Add depth limit (MAX_PLUGIN_ROOT_DEPTH=10) to findPluginRoots to
  prevent infinite recursion from symlink cycles
- Rename InstallFromZipResult to InstallFromSourceResult with type alias
  for backward compatibility
- Extract duplicate loadFirstPage logic in useMarketplaceBrowser hook
- Add documentation for intentionally disabled readContent and
  invalidateCache methods explaining API compatibility reasons

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* docs(plugin): add documentation for marketplace API and telemetry

- Document MARKETPLACE_API_BASE_URL with API endpoints and usage
- Add TODO for China mainland accessibility testing
- Document reportSkillInstall telemetry behavior and data transmitted

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(plugin): fix skill installation and improve UI

- Fix skill installation using v2 resolve API endpoint
- Extract base repo URL from GitHub tree/blob URLs
- Fix skill card type label (skill -> skills key mapping)
- Split plugin settings into "Available Plugins" and "Installed Plugins" tabs
- Fix refresh button styling with aspect-square
- Add card vertical spacing with pb-4
- Add unit tests for extractBaseRepoUrl and extractResolvedSkill

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor(plugin): use useTimer for search debounce

Replace manual setTimeout/clearTimeout with useTimer hook for better
timer management and automatic cleanup on component unmount.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* style(plugin): use explicit unknown type in catch blocks

Change all catch blocks from `catch (error)` to `catch (error: unknown)`
for better type safety and code clarity.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(plugin): only show content section for installed plugins

- Skip content fetching for marketplace plugins since readContent
  always fails for remote plugins
- Only display the Content section when viewing installed plugins
- Change catch (error) to catch (error: unknown) for type safety

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor(plugin): use Zod schemas for marketplace API responses

- Add PluginResolveResponseSchema for plugin resolution API
- Add ResolvedSkillSchema and SkillsResolveResponseSchema for v2 skills API
- Refactor extractRepositoryUrl to use Zod safeParse instead of manual type checking
- Refactor resolveSkillV2 to validate response with Zod and return typed array
- Update extractResolvedSkill to accept typed array directly
- Update tests to match new function signatures

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor(plugin): simplify code and reduce duplication

- Merge runCommand and captureCommand into executeCommand in PluginService
- Combine URL regex patterns and use constant map for plugin directories
- Create generic response parser factory in MarketplaceService
- Extract buildSkillSourceKey helper in useMarketplaceBrowser
- Remove unused displayedEntries variable in PluginBrowser
- Consolidate category config into single object in useResourcePanel
- Extract ensureCacheData helper method in PluginCacheStore

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(plugin): remove dead readContent and invalidateCache methods

Remove methods that were non-functional (always throwing or no-op):
- Remove invalidateCache() no-op method from PluginService
- Remove readContent() method that always throws
- Remove corresponding IPC handlers and preload bindings
- Remove IpcChannel enum entries for both methods
- Remove content section from PluginDetailModal (relied on readContent)
- Remove agentId prop from PluginBrowser (no longer needed)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor(plugin): update plugin settings and UI components

- Change default page size in useMarketplaceBrowser from 100 to 40 for improved performance.
- Add titles to the installed plugins section in multiple language files for better clarity.
- Refactor AgentSettings components to improve structure and readability, including the introduction of a new PluginsSettings component.
- Update modal widths in AgentSettingsPopup and SessionSettingsPopup for better UI consistency.
- Integrate Scrollbar component in SettingsContainer for enhanced scrolling experience.

* refactor(settings): improve Scrollbar import and enhance type safety

- Change Scrollbar import to use type-only import for ScrollbarProps.
- Update handleVirtualChange function to handle null scrollOffset for better type safety.

* fix(plugin): install only selected plugin and fix uninstall lookup

Bug fixes:
- Install only the specific requested plugin from marketplace repo
  instead of installing all plugins found in the repository
- Use actual installed filename for uninstall instead of marketplace
  metadata filename (which differs due to sanitization)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(plugin): support skill upload via drag-and-drop

Extended installFromSourceDir to detect and install skills:
- Check for plugin roots first (with .claude-plugin/plugin.json)
- If none found, search for skill directories (with SKILL.md)
- Install whichever type is detected

Added new methods:
- installSkillRoots: Install multiple skill directories
- installSkillFromDirectory: Install a single skill folder

Now users can drag-and-drop skill folders or ZIPs containing
skills to install them, not just plugin packages.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* test(browser): add beforeEach to reset mock state

Add vi.clearAllMocks() in beforeEach to prevent state leakage
between tests, which could cause flaky test failures in CI.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: kangfenmao <kangfenmao@qq.com>
2026-02-04 14:26:37 +08:00

127 lines
4.6 KiB
JavaScript

const { Arch } = require('electron-builder')
const { execSync } = require('child_process')
const fs = require('fs')
const path = require('path')
const { parse, stringify } = require('yaml')
const workspaceConfigPath = path.join(__dirname, '..', 'pnpm-workspace.yaml')
// if you want to add new prebuild binaries packages with different architectures, you can add them here
// please add to allX64 and allArm64 from pnpm-lock.yaml
const packages = [
'@img/sharp-darwin-arm64',
'@img/sharp-darwin-x64',
'@img/sharp-libvips-darwin-arm64',
'@img/sharp-libvips-darwin-x64',
'@img/sharp-libvips-linux-arm64',
'@img/sharp-libvips-linuxmusl-arm64',
'@img/sharp-libvips-linux-x64',
'@img/sharp-libvips-linuxmusl-x64',
'@img/sharp-linux-arm64',
'@img/sharp-linux-x64',
'@img/sharp-linuxmusl-arm64',
'@img/sharp-linuxmusl-x64',
'@img/sharp-win32-arm64',
'@img/sharp-win32-x64',
'@libsql/darwin-arm64',
'@libsql/darwin-x64',
'@libsql/linux-arm64-gnu',
'@libsql/linux-x64-gnu',
'@libsql/linux-arm64-musl',
'@libsql/linux-x64-musl',
'@libsql/win32-x64-msvc',
'@napi-rs/system-ocr-darwin-arm64',
'@napi-rs/system-ocr-darwin-x64',
'@napi-rs/system-ocr-win32-arm64-msvc',
'@napi-rs/system-ocr-win32-x64-msvc',
'@strongtz/win32-arm64-msvc'
]
const platformToArch = {
mac: 'darwin',
windows: 'win32',
linux: 'linux',
linuxmusl: 'linuxmusl'
}
exports.default = async function (context) {
const arch = context.arch === Arch.arm64 ? 'arm64' : 'x64'
const platformName = context.packager.platform.name
const platform = platformToArch[platformName]
const downloadPackages = async () => {
// Skip if target platform and architecture match current system
if (platform === process.platform && arch === process.arch) {
console.log(`Skipping install: target (${platform}/${arch}) matches current system`)
return
}
console.log(`Installing packages for target platform=${platform} arch=${arch}...`)
// Backup and modify pnpm-workspace.yaml to add target platform support
const originalWorkspaceConfig = fs.readFileSync(workspaceConfigPath, 'utf-8')
const workspaceConfig = parse(originalWorkspaceConfig)
// Add target platform to supportedArchitectures.os
if (!workspaceConfig.supportedArchitectures.os.includes(platform)) {
workspaceConfig.supportedArchitectures.os.push(platform)
}
// Add target architecture to supportedArchitectures.cpu
if (!workspaceConfig.supportedArchitectures.cpu.includes(arch)) {
workspaceConfig.supportedArchitectures.cpu.push(arch)
}
const modifiedWorkspaceConfig = stringify(workspaceConfig)
console.log('Modified workspace config:', modifiedWorkspaceConfig)
fs.writeFileSync(workspaceConfigPath, modifiedWorkspaceConfig)
try {
execSync(`pnpm install`, { stdio: 'inherit' })
} finally {
// Restore original pnpm-workspace.yaml
fs.writeFileSync(workspaceConfigPath, originalWorkspaceConfig)
}
}
await downloadPackages()
const excludePackages = async (packagesToExclude) => {
// 从项目根目录的 electron-builder.yml 读取 files 配置,避免多次覆盖配置导致出错
const electronBuilderConfigPath = path.join(__dirname, '..', 'electron-builder.yml')
const electronBuilderConfig = parse(fs.readFileSync(electronBuilderConfigPath, 'utf-8'))
let filters = electronBuilderConfig.files
// add filters for other architectures (exclude them)
filters.push(...packagesToExclude)
context.packager.config.files[0].filter = filters
}
const arm64KeepPackages = packages.filter((p) => p.includes('arm64') && p.includes(platform))
const arm64ExcludePackages = packages
.filter((p) => !arm64KeepPackages.includes(p))
.map((p) => '!node_modules/' + p + '/**')
const x64KeepPackages = packages.filter((p) => p.includes('x64') && p.includes(platform))
const x64ExcludePackages = packages
.filter((p) => !x64KeepPackages.includes(p))
.map((p) => '!node_modules/' + p + '/**')
const excludeRipgrepFilters = ['arm64-darwin', 'arm64-linux', 'x64-darwin', 'x64-linux', 'x64-win32']
.filter((f) => {
// On Windows ARM64, also keep x64-win32 for emulation compatibility
if (platform === 'win32' && context.arch === Arch.arm64 && f === 'x64-win32') {
return false
}
return f !== `${arch}-${platform}`
})
.map((f) => '!node_modules/@anthropic-ai/claude-agent-sdk/vendor/ripgrep/' + f + '/**')
if (context.arch === Arch.arm64) {
await excludePackages([...arm64ExcludePackages, ...excludeRipgrepFilters])
} else {
await excludePackages([...x64ExcludePackages, ...excludeRipgrepFilters])
}
}