- 在多个协议实现中(如ProjectProtocol、PackageProtocol等)引入DirectoryService,替换了直接的路径处理逻辑,增强了路径解析的智能性和可靠性。 - 更新了相关方法以支持异步操作,确保在查找项目根目录和注册表路径时能够优雅地处理错误并回退到默认路径。 - 在PromptXConfig中动态计算.promptx目录路径,提升了配置管理的灵活性。 此改动旨在提升代码的可读性和一致性,同时为未来的扩展打下基础。
609 lines
16 KiB
JavaScript
609 lines
16 KiB
JavaScript
const path = require('path')
|
||
const fs = require('fs')
|
||
const fsPromises = require('fs').promises
|
||
const ResourceProtocol = require('./ResourceProtocol')
|
||
const { QueryParams } = require('../types')
|
||
const logger = require('../../../utils/logger')
|
||
const { getDirectoryService } = require('../../../utils/DirectoryService')
|
||
|
||
/**
|
||
* 包协议实现
|
||
* 实现@package://协议,智能检测并访问NPM包资源
|
||
* 支持:本地开发、npm install、npm -g、npx、monorepo等场景
|
||
*/
|
||
class PackageProtocol extends ResourceProtocol {
|
||
constructor (options = {}) {
|
||
super('package', options)
|
||
|
||
// 包安装模式检测缓存
|
||
this.installModeCache = new Map()
|
||
this.directoryService = getDirectoryService()
|
||
}
|
||
|
||
/**
|
||
* 设置注册表(保持与其他协议的一致性)
|
||
*/
|
||
setRegistry (registry) {
|
||
// Package协议不使用注册表,但为了一致性提供此方法
|
||
this.registry = registry || {}
|
||
}
|
||
|
||
/**
|
||
* 获取协议信息
|
||
*/
|
||
getProtocolInfo () {
|
||
return {
|
||
name: this.name,
|
||
description: '包协议 - 智能访问NPM包资源,支持多种安装模式',
|
||
examples: [
|
||
'@package://package.json',
|
||
'@package://src/index.js',
|
||
'@package://docs/README.md',
|
||
'@package://prompt/core/thought.md',
|
||
'@package://templates/basic/template.md'
|
||
],
|
||
installModes: [
|
||
'development', // 开发模式
|
||
'local', // 本地npm install
|
||
'global', // 全局npm install -g
|
||
'npx', // npx执行
|
||
'monorepo', // monorepo workspace
|
||
'link' // npm link
|
||
]
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 检测当前包安装模式
|
||
*/
|
||
async detectInstallMode () {
|
||
const cacheKey = 'currentInstallMode'
|
||
if (this.installModeCache.has(cacheKey)) {
|
||
return this.installModeCache.get(cacheKey)
|
||
}
|
||
|
||
const mode = await this._performInstallModeDetection()
|
||
this.installModeCache.set(cacheKey, mode)
|
||
return mode
|
||
}
|
||
|
||
/**
|
||
* 执行安装模式检测
|
||
*/
|
||
async _performInstallModeDetection () {
|
||
let cwd
|
||
try {
|
||
const context = {
|
||
startDir: process.cwd(),
|
||
platform: process.platform,
|
||
avoidUserHome: true
|
||
}
|
||
cwd = await this.directoryService.getProjectRoot(context)
|
||
} catch (error) {
|
||
cwd = process.cwd()
|
||
}
|
||
|
||
const execPath = process.argv[0]
|
||
const scriptPath = process.argv[1]
|
||
|
||
// 检测npx执行
|
||
if (this._isNpxExecution()) {
|
||
return 'npx'
|
||
}
|
||
|
||
// 检测全局安装
|
||
if (this._isGlobalInstall()) {
|
||
return 'global'
|
||
}
|
||
|
||
// 检测开发模式
|
||
if (this._isDevelopmentMode()) {
|
||
return 'development'
|
||
}
|
||
|
||
// 检测monorepo
|
||
if (this._isMonorepoWorkspace()) {
|
||
return 'monorepo'
|
||
}
|
||
|
||
// 检测npm link
|
||
if (this._isNpmLink()) {
|
||
return 'link'
|
||
}
|
||
|
||
// 默认为本地安装
|
||
return 'local'
|
||
}
|
||
|
||
/**
|
||
* 检测是否是npx执行
|
||
*/
|
||
_isNpxExecution () {
|
||
// 标准化环境变量路径(处理Windows反斜杠)
|
||
const normalizeEnvPath = (envPath) => {
|
||
return envPath ? envPath.replace(/\\/g, '/').toLowerCase() : ''
|
||
}
|
||
|
||
// 检查环境变量 - Windows和Unix兼容
|
||
if (process.env.npm_execpath) {
|
||
const normalizedExecPath = normalizeEnvPath(process.env.npm_execpath)
|
||
if (normalizedExecPath.includes('npx')) {
|
||
return true
|
||
}
|
||
}
|
||
|
||
// 检查npm_config_cache路径 - Windows和Unix兼容
|
||
if (process.env.npm_config_cache) {
|
||
const normalizedCachePath = normalizeEnvPath(process.env.npm_config_cache)
|
||
if (normalizedCachePath.includes('_npx')) {
|
||
return true
|
||
}
|
||
}
|
||
|
||
// 检查执行路径 - Windows和Unix兼容
|
||
const scriptPath = process.argv[1]
|
||
if (scriptPath) {
|
||
const normalizedScriptPath = normalizeEnvPath(scriptPath)
|
||
if (normalizedScriptPath.includes('_npx')) {
|
||
return true
|
||
}
|
||
}
|
||
|
||
// Windows特定检查:检查.cmd或.bat文件
|
||
if (process.platform === 'win32') {
|
||
// 检查是否通过npx.cmd执行
|
||
if (process.env.npm_execpath &&
|
||
(process.env.npm_execpath.endsWith('npx.cmd') ||
|
||
process.env.npm_execpath.endsWith('npx.bat'))) {
|
||
return true
|
||
}
|
||
|
||
// Windows NPX缓存目录通常包含_npx
|
||
const windowsNpxPaths = [
|
||
process.env.LOCALAPPDATA && path.join(process.env.LOCALAPPDATA, 'npm-cache', '_npx'),
|
||
process.env.APPDATA && path.join(process.env.APPDATA, 'npm', '_npx'),
|
||
process.env.TEMP && path.join(process.env.TEMP, '_npx')
|
||
].filter(Boolean)
|
||
|
||
const currentPath = __dirname
|
||
if (windowsNpxPaths.some(npxPath => currentPath.includes(npxPath))) {
|
||
return true
|
||
}
|
||
}
|
||
|
||
return false
|
||
}
|
||
|
||
/**
|
||
* 检测是否是全局安装
|
||
*/
|
||
_isGlobalInstall () {
|
||
const currentPath = __dirname
|
||
|
||
// 常见全局安装路径
|
||
const globalPaths = [
|
||
'/usr/lib/node_modules',
|
||
'/usr/local/lib/node_modules',
|
||
'/opt/homebrew/lib/node_modules',
|
||
path.join(process.env.HOME || '', '.npm-global'),
|
||
path.join(process.env.APPDATA || '', 'npm', 'node_modules'),
|
||
path.join(process.env.PREFIX || '', 'lib', 'node_modules')
|
||
]
|
||
|
||
return globalPaths.some(globalPath =>
|
||
currentPath.startsWith(globalPath)
|
||
)
|
||
}
|
||
|
||
/**
|
||
* 检测是否是开发模式
|
||
*/
|
||
_isDevelopmentMode () {
|
||
// 检查NODE_ENV
|
||
if (process.env.NODE_ENV === 'development') {
|
||
return true
|
||
}
|
||
|
||
// 检查是否在node_modules外
|
||
const currentPath = __dirname
|
||
if (!currentPath.includes('node_modules')) {
|
||
return true
|
||
}
|
||
|
||
// 检查package.json中的main字段是否指向源文件
|
||
try {
|
||
const packageJsonPath = this.findPackageJson()
|
||
if (packageJsonPath) {
|
||
const packageJson = require(packageJsonPath)
|
||
const mainFile = packageJson.main || 'index.js'
|
||
return mainFile.startsWith('src/') || mainFile.startsWith('lib/')
|
||
}
|
||
} catch (error) {
|
||
// 忽略错误,继续其他检测
|
||
}
|
||
|
||
return false
|
||
}
|
||
|
||
/**
|
||
* 检测是否是monorepo workspace
|
||
*/
|
||
_isMonorepoWorkspace () {
|
||
try {
|
||
const packageJsonPath = this.findPackageJson()
|
||
if (packageJsonPath) {
|
||
const packageJson = require(packageJsonPath)
|
||
|
||
// 检查workspaces字段
|
||
if (packageJson.workspaces) {
|
||
return true
|
||
}
|
||
|
||
// 检查是否在workspace包内
|
||
const rootPackageJsonPath = this.findRootPackageJson()
|
||
if (rootPackageJsonPath && rootPackageJsonPath !== packageJsonPath) {
|
||
const rootPackageJson = require(rootPackageJsonPath)
|
||
return !!rootPackageJson.workspaces
|
||
}
|
||
}
|
||
} catch (error) {
|
||
// 忽略错误
|
||
}
|
||
|
||
return false
|
||
}
|
||
|
||
/**
|
||
* 检测是否是npm link
|
||
*/
|
||
_isNpmLink () {
|
||
try {
|
||
const currentPath = __dirname
|
||
const stats = require('fs').lstatSync(currentPath)
|
||
return stats.isSymbolicLink()
|
||
} catch (error) {
|
||
return false
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 查找package.json文件
|
||
*/
|
||
findPackageJson (startPath = __dirname) {
|
||
let currentPath = path.resolve(startPath)
|
||
|
||
let maxIterations = 50 // Prevent infinite loops
|
||
while (currentPath !== path.parse(currentPath).root && maxIterations-- > 0) {
|
||
const packageJsonPath = path.join(currentPath, 'package.json')
|
||
if (require('fs').existsSync(packageJsonPath)) {
|
||
return packageJsonPath
|
||
}
|
||
const parentPath = path.dirname(currentPath)
|
||
if (parentPath === currentPath) {
|
||
break // Additional protection
|
||
}
|
||
currentPath = parentPath
|
||
}
|
||
|
||
return null
|
||
}
|
||
|
||
/**
|
||
* 查找根package.json文件(用于monorepo检测)
|
||
*/
|
||
findRootPackageJson () {
|
||
let currentPath = process.cwd()
|
||
let lastValidPackageJson = null
|
||
let maxIterations = 50 // Prevent infinite loops
|
||
|
||
while (currentPath !== path.parse(currentPath).root && maxIterations-- > 0) {
|
||
const packageJsonPath = path.join(currentPath, 'package.json')
|
||
if (require('fs').existsSync(packageJsonPath)) {
|
||
lastValidPackageJson = packageJsonPath
|
||
}
|
||
const parentPath = path.dirname(currentPath)
|
||
if (parentPath === currentPath) {
|
||
break // Additional protection
|
||
}
|
||
currentPath = parentPath
|
||
}
|
||
|
||
return lastValidPackageJson
|
||
}
|
||
|
||
/**
|
||
* 获取包根目录
|
||
*/
|
||
async getPackageRoot () {
|
||
const mode = this.detectInstallMode()
|
||
|
||
switch (mode) {
|
||
case 'development':
|
||
// 开发模式:查找项目根目录
|
||
return this._findProjectRoot()
|
||
|
||
case 'global':
|
||
// 全局安装:查找全局包目录
|
||
return this._findGlobalPackageRoot()
|
||
|
||
case 'npx':
|
||
// npx:查找临时包目录
|
||
return this._findNpxPackageRoot()
|
||
|
||
case 'monorepo':
|
||
// monorepo:查找workspace包目录
|
||
return this._findWorkspacePackageRoot()
|
||
|
||
case 'link':
|
||
// npm link:解析符号链接
|
||
return this._findLinkedPackageRoot()
|
||
|
||
case 'local':
|
||
default:
|
||
// 本地安装:查找node_modules中的包目录
|
||
return this._findLocalPackageRoot()
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 查找项目根目录
|
||
*/
|
||
_findProjectRoot () {
|
||
const packageJsonPath = this.findPackageJson()
|
||
return packageJsonPath ? path.dirname(packageJsonPath) : process.cwd()
|
||
}
|
||
|
||
/**
|
||
* 查找全局包根目录
|
||
*/
|
||
_findGlobalPackageRoot () {
|
||
// 从当前模块路径向上查找,直到找到package.json
|
||
return this._findProjectRoot()
|
||
}
|
||
|
||
/**
|
||
* 查找npx包根目录
|
||
*/
|
||
_findNpxPackageRoot () {
|
||
// npx通常将包缓存在特定目录
|
||
const packageJsonPath = this.findPackageJson()
|
||
return packageJsonPath ? path.dirname(packageJsonPath) : process.cwd()
|
||
}
|
||
|
||
/**
|
||
* 查找workspace包根目录
|
||
*/
|
||
_findWorkspacePackageRoot () {
|
||
// 在monorepo中查找当前workspace的根目录
|
||
return this._findProjectRoot()
|
||
}
|
||
|
||
/**
|
||
* 查找链接包根目录
|
||
*/
|
||
_findLinkedPackageRoot () {
|
||
try {
|
||
// 解析符号链接
|
||
const realPath = require('fs').realpathSync(__dirname)
|
||
const packageJsonPath = this.findPackageJson(realPath)
|
||
return packageJsonPath ? path.dirname(packageJsonPath) : realPath
|
||
} catch (error) {
|
||
return this._findProjectRoot()
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 查找本地包根目录
|
||
*/
|
||
_findLocalPackageRoot () {
|
||
// 在node_modules中查找包根目录
|
||
return this._findProjectRoot()
|
||
}
|
||
|
||
/**
|
||
* 解析路径到具体的文件系统路径
|
||
* @param {string} relativePath - 相对于包根目录的路径
|
||
* @param {QueryParams} params - 查询参数
|
||
* @returns {Promise<string>} 解析后的绝对路径
|
||
*/
|
||
async resolvePath (relativePath, params = null) {
|
||
// 生成缓存键
|
||
const cacheKey = `resolve:${relativePath}:${params ? params.toString() : ''}`
|
||
|
||
// 检查缓存
|
||
if (this.cache.has(cacheKey)) {
|
||
return this.cache.get(cacheKey)
|
||
}
|
||
|
||
// 获取包根目录
|
||
const packageRoot = await this.getPackageRoot()
|
||
|
||
// 验证路径是否在package.json的files字段中
|
||
this.validateFileAccess(packageRoot, relativePath)
|
||
|
||
// 直接处理路径,不需要目录映射
|
||
const relativePathClean = relativePath.replace(/^\/+/, '')
|
||
const fullPath = path.resolve(packageRoot, relativePathClean)
|
||
|
||
// 安全检查:确保路径在包根目录内
|
||
if (!fullPath.startsWith(packageRoot)) {
|
||
throw new Error(`路径安全检查失败: ${relativePath}`)
|
||
}
|
||
|
||
// 存储到缓存
|
||
this.cache.set(cacheKey, fullPath)
|
||
|
||
return fullPath
|
||
}
|
||
|
||
/**
|
||
* 验证文件访问权限(基于package.json的files字段)
|
||
* @param {string} packageRoot - 包根目录
|
||
* @param {string} relativePath - 相对路径
|
||
*/
|
||
validateFileAccess (packageRoot, relativePath) {
|
||
try {
|
||
const packageJsonPath = path.join(packageRoot, 'package.json')
|
||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
|
||
|
||
// 如果没有files字段,允许访问所有文件(开发模式)
|
||
if (!packageJson.files || !Array.isArray(packageJson.files)) {
|
||
return
|
||
}
|
||
|
||
// 使用Node.js原生API进行跨平台路径规范化
|
||
const normalizedPath = this.normalizePathForComparison(relativePath)
|
||
|
||
// 检查是否匹配files字段中的任何模式
|
||
const isAllowed = packageJson.files.some(filePattern => {
|
||
// 标准化文件模式
|
||
const normalizedPattern = this.normalizePathForComparison(filePattern)
|
||
|
||
// 精确匹配
|
||
if (normalizedPattern === normalizedPath) {
|
||
return true
|
||
}
|
||
|
||
// 目录匹配(以/结尾或包含/*)
|
||
if (normalizedPattern.endsWith('/') || normalizedPattern.endsWith('/*')) {
|
||
const dirPattern = normalizedPattern.replace(/\/?\*?$/, '/')
|
||
return normalizedPath.startsWith(dirPattern)
|
||
}
|
||
|
||
// 通配符匹配
|
||
if (normalizedPattern.includes('*')) {
|
||
const regexPattern = normalizedPattern
|
||
.replace(/\./g, '\\.')
|
||
.replace(/\*/g, '.*')
|
||
const regex = new RegExp(`^${regexPattern}$`)
|
||
return regex.test(normalizedPath)
|
||
}
|
||
|
||
// 目录前缀匹配
|
||
if (normalizedPath.startsWith(normalizedPattern + '/')) {
|
||
return true
|
||
}
|
||
|
||
return false
|
||
})
|
||
|
||
if (!isAllowed) {
|
||
// 在生产环境严格检查,开发环境只警告
|
||
const installMode = this.detectInstallMode()
|
||
if (installMode === 'development' || installMode === 'npx') {
|
||
logger.warn(`⚠️ Warning: Path '${relativePath}' not in package.json files field. This may cause issues after publishing.`)
|
||
} else {
|
||
throw new Error(`Access denied: Path '${relativePath}' is not included in package.json files field`)
|
||
}
|
||
}
|
||
} catch (error) {
|
||
// 如果读取package.json失败,在开发模式下允许访问
|
||
const installMode = this.detectInstallMode()
|
||
if (installMode === 'development' || installMode === 'npx') {
|
||
logger.warn(`⚠️ Warning: Could not validate file access for '${relativePath}': ${error.message}`)
|
||
} else {
|
||
throw error
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 跨平台路径规范化函数
|
||
* @param {string} inputPath - 输入路径
|
||
* @returns {string} 规范化后的路径
|
||
*/
|
||
normalizePathForComparison (inputPath) {
|
||
if (!inputPath || typeof inputPath !== 'string') {
|
||
return ''
|
||
}
|
||
|
||
// 使用Node.js原生API进行路径规范化
|
||
let normalized = path.normalize(inputPath)
|
||
|
||
// 统一使用正斜杠进行比较(但不破坏实际的文件系统操作)
|
||
normalized = normalized.replace(/\\/g, '/')
|
||
|
||
// 移除开头的斜杠
|
||
normalized = normalized.replace(/^\/+/, '')
|
||
|
||
return normalized
|
||
}
|
||
|
||
/**
|
||
* 检查资源是否存在
|
||
*/
|
||
async exists (resourcePath, queryParams) {
|
||
try {
|
||
const resolvedPath = await this.resolvePath(resourcePath, queryParams)
|
||
await fsPromises.access(resolvedPath)
|
||
return true
|
||
} catch (error) {
|
||
return false
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 加载资源内容
|
||
* @param {string} resolvedPath - 已解析的路径
|
||
* @param {QueryParams} [queryParams] - 查询参数
|
||
* @returns {Object} 包含内容和元数据的对象
|
||
*/
|
||
async loadContent (resolvedPath, queryParams) {
|
||
try {
|
||
await fsPromises.access(resolvedPath)
|
||
const content = await fsPromises.readFile(resolvedPath, 'utf8')
|
||
const stats = await fsPromises.stat(resolvedPath)
|
||
const packageRoot = await this.getPackageRoot()
|
||
|
||
return {
|
||
content,
|
||
path: resolvedPath,
|
||
protocol: 'package',
|
||
installMode: this.detectInstallMode(),
|
||
metadata: {
|
||
size: content.length,
|
||
lastModified: stats.mtime,
|
||
absolutePath: resolvedPath,
|
||
relativePath: path.relative(packageRoot, resolvedPath)
|
||
}
|
||
}
|
||
} catch (error) {
|
||
if (error.code === 'ENOENT') {
|
||
throw new Error(`包资源不存在: ${resolvedPath}`)
|
||
}
|
||
throw new Error(`加载包资源失败: ${error.message}`)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取调试信息
|
||
*/
|
||
getDebugInfo () {
|
||
const mode = this.detectInstallMode()
|
||
|
||
return {
|
||
protocol: this.name,
|
||
installMode: mode,
|
||
packageRoot: this.getPackageRoot(),
|
||
currentWorkingDirectory: process.cwd(),
|
||
moduleDirectory: __dirname,
|
||
environment: {
|
||
NODE_ENV: process.env.NODE_ENV,
|
||
npm_execpath: process.env.npm_execpath,
|
||
npm_config_cache: process.env.npm_config_cache
|
||
},
|
||
cacheSize: this.cache.size
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 清理缓存
|
||
*/
|
||
clearCache () {
|
||
super.clearCache()
|
||
this.installModeCache.clear()
|
||
}
|
||
}
|
||
|
||
module.exports = PackageProtocol
|