重构:引入统一的DirectoryService以优化目录管理
- 在InitCommand、RecallCommand、RememberCommand和PouchStateMachine中替换了直接路径处理逻辑,改为使用DirectoryService进行目录解析。 - 更新了ProjectDiscovery以使用新的getProjectRoot方法,标记旧方法为已弃用。 - 在executionContext中重构了工作目录获取逻辑,增强了兼容性和可维护性。 - 确保了对用户主目录的避免处理,提升了目录定位的智能性和可靠性。 此改动旨在提升代码的可读性和一致性,同时为未来的扩展打下基础。
This commit is contained in:
460
src/lib/utils/DirectoryLocator.js
Normal file
460
src/lib/utils/DirectoryLocator.js
Normal file
@ -0,0 +1,460 @@
|
||||
const fs = require('fs-extra')
|
||||
const path = require('path')
|
||||
const os = require('os')
|
||||
|
||||
/**
|
||||
* 目录定位器基础抽象类
|
||||
* 统一管理所有路径解析逻辑,支持跨平台差异化实现
|
||||
*/
|
||||
class DirectoryLocator {
|
||||
constructor(options = {}) {
|
||||
this.options = options
|
||||
this.cache = new Map()
|
||||
this.platform = process.platform
|
||||
}
|
||||
|
||||
/**
|
||||
* 抽象方法:定位目录
|
||||
* @param {Object} context - 定位上下文
|
||||
* @returns {Promise<string>} 定位到的目录路径
|
||||
*/
|
||||
async locate(context = {}) {
|
||||
throw new Error('子类必须实现 locate 方法')
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存
|
||||
*/
|
||||
getCached(key) {
|
||||
return this.cache.get(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置缓存
|
||||
*/
|
||||
setCached(key, value) {
|
||||
this.cache.set(key, value)
|
||||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除缓存
|
||||
*/
|
||||
clearCache() {
|
||||
this.cache.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查路径是否存在且是目录
|
||||
*/
|
||||
async isValidDirectory(dirPath) {
|
||||
try {
|
||||
const stat = await fs.stat(dirPath)
|
||||
return stat.isDirectory()
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化路径
|
||||
*/
|
||||
normalizePath(inputPath) {
|
||||
if (!inputPath || typeof inputPath !== 'string') {
|
||||
return null
|
||||
}
|
||||
return path.resolve(inputPath)
|
||||
}
|
||||
|
||||
/**
|
||||
* 展开家目录路径
|
||||
*/
|
||||
expandHome(filepath) {
|
||||
if (!filepath || typeof filepath !== 'string') {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (filepath.startsWith('~/') || filepath === '~') {
|
||||
return path.join(os.homedir(), filepath.slice(2))
|
||||
}
|
||||
|
||||
return filepath
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 项目根目录定位器
|
||||
* 负责查找项目的根目录
|
||||
*/
|
||||
class ProjectRootLocator extends DirectoryLocator {
|
||||
constructor(options = {}) {
|
||||
super(options)
|
||||
|
||||
// 可配置的查找策略优先级
|
||||
this.strategies = options.strategies || [
|
||||
'existingPromptxDirectory',
|
||||
'currentWorkingDirectoryIfHasMarkers',
|
||||
'packageJsonDirectory',
|
||||
'gitRootDirectory',
|
||||
'currentWorkingDirectory'
|
||||
]
|
||||
|
||||
// 项目标识文件
|
||||
this.projectMarkers = options.projectMarkers || [
|
||||
'package.json',
|
||||
'.git',
|
||||
'pyproject.toml',
|
||||
'Cargo.toml',
|
||||
'pom.xml',
|
||||
'build.gradle',
|
||||
'composer.json'
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* 定位项目根目录
|
||||
*/
|
||||
async locate(context = {}) {
|
||||
const { startDir = process.cwd() } = context
|
||||
const cacheKey = `projectRoot:${startDir}`
|
||||
|
||||
// 检查缓存
|
||||
const cached = this.getCached(cacheKey)
|
||||
if (cached) {
|
||||
return cached
|
||||
}
|
||||
|
||||
// 使用上下文中的策略或默认策略
|
||||
const strategies = context.strategies || this.strategies
|
||||
|
||||
// 按策略优先级查找
|
||||
for (const strategy of strategies) {
|
||||
const result = await this._executeStrategy(strategy, startDir, context)
|
||||
if (result && await this._validateProjectRoot(result, context)) {
|
||||
return this.setCached(cacheKey, result)
|
||||
}
|
||||
}
|
||||
|
||||
// 如果所有策略都失败,返回起始目录
|
||||
return this.setCached(cacheKey, startDir)
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行特定的查找策略
|
||||
*/
|
||||
async _executeStrategy(strategy, startDir, context) {
|
||||
switch (strategy) {
|
||||
case 'existingPromptxDirectory':
|
||||
return await this._findByExistingPromptx(startDir)
|
||||
|
||||
case 'currentWorkingDirectoryIfHasMarkers':
|
||||
return await this._checkCurrentDirForMarkers(startDir)
|
||||
|
||||
case 'packageJsonDirectory':
|
||||
return await this._findByProjectMarkers(startDir)
|
||||
|
||||
case 'gitRootDirectory':
|
||||
return await this._findByGitRoot(startDir)
|
||||
|
||||
case 'currentWorkingDirectory':
|
||||
return startDir
|
||||
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查当前目录是否包含项目标识文件
|
||||
*/
|
||||
async _checkCurrentDirForMarkers(startDir) {
|
||||
const currentDir = path.resolve(startDir)
|
||||
|
||||
// 检查当前目录是否包含项目标识文件
|
||||
for (const marker of this.projectMarkers) {
|
||||
const markerPath = path.join(currentDir, marker)
|
||||
if (await fs.pathExists(markerPath)) {
|
||||
return currentDir
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过现有.promptx目录查找
|
||||
*/
|
||||
async _findByExistingPromptx(startDir) {
|
||||
let currentDir = path.resolve(startDir)
|
||||
const root = path.parse(currentDir).root
|
||||
|
||||
while (currentDir !== root) {
|
||||
const promptxPath = path.join(currentDir, '.promptx')
|
||||
if (await this.isValidDirectory(promptxPath)) {
|
||||
return currentDir
|
||||
}
|
||||
|
||||
const parentDir = path.dirname(currentDir)
|
||||
if (parentDir === currentDir) break
|
||||
currentDir = parentDir
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过项目标识文件查找
|
||||
*/
|
||||
async _findByProjectMarkers(startDir) {
|
||||
let currentDir = path.resolve(startDir)
|
||||
const root = path.parse(currentDir).root
|
||||
|
||||
while (currentDir !== root) {
|
||||
for (const marker of this.projectMarkers) {
|
||||
const markerPath = path.join(currentDir, marker)
|
||||
if (await fs.pathExists(markerPath)) {
|
||||
return currentDir
|
||||
}
|
||||
}
|
||||
|
||||
const parentDir = path.dirname(currentDir)
|
||||
if (parentDir === currentDir) break
|
||||
currentDir = parentDir
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过Git根目录查找
|
||||
*/
|
||||
async _findByGitRoot(startDir) {
|
||||
let currentDir = path.resolve(startDir)
|
||||
const root = path.parse(currentDir).root
|
||||
|
||||
while (currentDir !== root) {
|
||||
const gitPath = path.join(currentDir, '.git')
|
||||
if (await fs.pathExists(gitPath)) {
|
||||
return currentDir
|
||||
}
|
||||
|
||||
const parentDir = path.dirname(currentDir)
|
||||
if (parentDir === currentDir) break
|
||||
currentDir = parentDir
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证项目根目录
|
||||
*/
|
||||
async _validateProjectRoot(projectRoot, context = {}) {
|
||||
// Windows平台:避免用户家目录
|
||||
if (this.platform === 'win32' && context.avoidUserHome !== false) {
|
||||
const homeDir = os.homedir()
|
||||
if (path.resolve(projectRoot) === path.resolve(homeDir)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return await this.isValidDirectory(projectRoot)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PromptX工作空间定位器
|
||||
* 负责确定.promptx目录的位置
|
||||
*/
|
||||
class PromptXWorkspaceLocator extends DirectoryLocator {
|
||||
constructor(options = {}) {
|
||||
super(options)
|
||||
this.projectRootLocator = options.projectRootLocator || new ProjectRootLocator(options)
|
||||
}
|
||||
|
||||
/**
|
||||
* 定位PromptX工作空间
|
||||
*/
|
||||
async locate(context = {}) {
|
||||
const cacheKey = `promptxWorkspace:${JSON.stringify(context)}`
|
||||
|
||||
// 检查缓存
|
||||
const cached = this.getCached(cacheKey)
|
||||
if (cached) {
|
||||
return cached
|
||||
}
|
||||
|
||||
// 策略1:IDE环境变量
|
||||
const workspaceFromIDE = await this._fromIDEEnvironment()
|
||||
if (workspaceFromIDE) {
|
||||
return this.setCached(cacheKey, workspaceFromIDE)
|
||||
}
|
||||
|
||||
// 策略2:PromptX专用环境变量
|
||||
const workspaceFromEnv = await this._fromPromptXEnvironment()
|
||||
if (workspaceFromEnv) {
|
||||
return this.setCached(cacheKey, workspaceFromEnv)
|
||||
}
|
||||
|
||||
// 策略3:如果上下文指定了特定策略(如init命令),直接使用项目根目录
|
||||
if (context.strategies) {
|
||||
const workspaceFromProject = await this._fromProjectRoot(context)
|
||||
if (workspaceFromProject) {
|
||||
return this.setCached(cacheKey, workspaceFromProject)
|
||||
}
|
||||
}
|
||||
|
||||
// 策略4:现有.promptx目录
|
||||
const workspaceFromExisting = await this._fromExistingDirectory(context.startDir)
|
||||
if (workspaceFromExisting) {
|
||||
return this.setCached(cacheKey, workspaceFromExisting)
|
||||
}
|
||||
|
||||
// 策略5:项目根目录
|
||||
const workspaceFromProject = await this._fromProjectRoot(context)
|
||||
if (workspaceFromProject) {
|
||||
return this.setCached(cacheKey, workspaceFromProject)
|
||||
}
|
||||
|
||||
// 策略6:回退到当前目录
|
||||
return this.setCached(cacheKey, context.startDir || process.cwd())
|
||||
}
|
||||
|
||||
/**
|
||||
* 从IDE环境变量获取
|
||||
*/
|
||||
async _fromIDEEnvironment() {
|
||||
const workspaceFolders = process.env.WORKSPACE_FOLDER_PATHS
|
||||
if (workspaceFolders) {
|
||||
try {
|
||||
const folders = JSON.parse(workspaceFolders)
|
||||
if (Array.isArray(folders) && folders.length > 0) {
|
||||
const firstFolder = folders[0]
|
||||
if (await this.isValidDirectory(firstFolder)) {
|
||||
return firstFolder
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// 忽略解析错误
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* 从PromptX环境变量获取
|
||||
*/
|
||||
async _fromPromptXEnvironment() {
|
||||
const promptxWorkspaceEnv = process.env.PROMPTX_WORKSPACE
|
||||
if (promptxWorkspaceEnv && promptxWorkspaceEnv.trim() !== '') {
|
||||
const workspacePath = this.normalizePath(this.expandHome(promptxWorkspaceEnv))
|
||||
if (workspacePath && await this.isValidDirectory(workspacePath)) {
|
||||
return workspacePath
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* 从现有.promptx目录获取
|
||||
*/
|
||||
async _fromExistingDirectory(startDir) {
|
||||
const projectRoot = await this.projectRootLocator._findByExistingPromptx(startDir || process.cwd())
|
||||
return projectRoot
|
||||
}
|
||||
|
||||
/**
|
||||
* 从项目根目录获取
|
||||
*/
|
||||
async _fromProjectRoot(context) {
|
||||
const projectRoot = await this.projectRootLocator.locate(context)
|
||||
return projectRoot
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 目录定位器工厂
|
||||
*/
|
||||
class DirectoryLocatorFactory {
|
||||
/**
|
||||
* 创建项目根目录定位器
|
||||
*/
|
||||
static createProjectRootLocator(options = {}) {
|
||||
const platform = process.platform
|
||||
|
||||
// 根据平台创建特定实现
|
||||
if (platform === 'win32') {
|
||||
return new WindowsProjectRootLocator(options)
|
||||
} else {
|
||||
return new ProjectRootLocator(options)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建PromptX工作空间定位器
|
||||
*/
|
||||
static createPromptXWorkspaceLocator(options = {}) {
|
||||
const projectRootLocator = this.createProjectRootLocator(options)
|
||||
return new PromptXWorkspaceLocator({
|
||||
...options,
|
||||
projectRootLocator
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取平台信息
|
||||
*/
|
||||
static getPlatform() {
|
||||
return process.platform
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Windows平台的项目根目录定位器
|
||||
* 特殊处理Windows环境下的路径问题
|
||||
*/
|
||||
class WindowsProjectRootLocator extends ProjectRootLocator {
|
||||
constructor(options = {}) {
|
||||
super({
|
||||
...options,
|
||||
// Windows默认避免用户家目录
|
||||
avoidUserHome: options.avoidUserHome !== false
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Windows特有的项目根目录验证
|
||||
*/
|
||||
async _validateProjectRoot(projectRoot, context = {}) {
|
||||
// 调用基类验证
|
||||
const baseValid = await super._validateProjectRoot(projectRoot, context)
|
||||
if (!baseValid) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Windows特有:避免系统关键目录
|
||||
const systemPaths = [
|
||||
'C:\\Windows',
|
||||
'C:\\Program Files',
|
||||
'C:\\Program Files (x86)',
|
||||
'C:\\System Volume Information'
|
||||
]
|
||||
|
||||
const resolvedPath = path.resolve(projectRoot).toUpperCase()
|
||||
for (const systemPath of systemPaths) {
|
||||
if (resolvedPath.startsWith(systemPath.toUpperCase())) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
DirectoryLocator,
|
||||
ProjectRootLocator,
|
||||
PromptXWorkspaceLocator,
|
||||
DirectoryLocatorFactory,
|
||||
WindowsProjectRootLocator
|
||||
}
|
||||
Reference in New Issue
Block a user