重构:引入统一的DirectoryService以优化目录管理
- 在InitCommand、RecallCommand、RememberCommand和PouchStateMachine中替换了直接路径处理逻辑,改为使用DirectoryService进行目录解析。 - 更新了ProjectDiscovery以使用新的getProjectRoot方法,标记旧方法为已弃用。 - 在executionContext中重构了工作目录获取逻辑,增强了兼容性和可维护性。 - 确保了对用户主目录的避免处理,提升了目录定位的智能性和可靠性。 此改动旨在提升代码的可读性和一致性,同时为未来的扩展打下基础。
This commit is contained in:
73
docs/issues/new 1.MD
Normal file
73
docs/issues/new 1.MD
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
PS C:\Users\Administrator\Desktop\LUCKY> npx -f -y --registry=https://registry.npmjs.org dpml-prompt@snapshot -v
|
||||||
|
npm warn using --force Recommended protections disabled.
|
||||||
|
0.0.2-snapshot.20250614141120.2d90a70
|
||||||
|
PS C:\Users\Administrator\Desktop\LUCKY> npx -y -f dpml-prompt@snapshot init
|
||||||
|
npm warn using --force Recommended protections disabled.
|
||||||
|
▶️ 正在扫描项目资源...
|
||||||
|
ℹ [ProjectDiscovery] ✅ 项目注册表生成完成,发现 0 个资源
|
||||||
|
ℹ [PackageDiscovery] ✅ 硬编码注册表加载成功,发现 45 个资源
|
||||||
|
ℹ [PackageDiscovery] 📋 包级角色资源: package:assistant, package:frontend-developer, package:java-backend-developer, package:product-manager, package:xiaohongshu-marketer, package:nuwa, assistant, frontend-developer, java-backend-developer, product-manager, xiaohongshu-marketer, nuwa
|
||||||
|
ℹ [ProjectDiscovery] 📋 项目注册表无效,重新生成
|
||||||
|
ℹ [ProjectDiscovery] ✅ 项目注册表生成完成,发现 0 个资源
|
||||||
|
|
||||||
|
============================================================
|
||||||
|
🎯 锦囊目的:初始化PromptX工作环境,创建必要的配置目录和文件,生成项目级资源注册表
|
||||||
|
============================================================
|
||||||
|
|
||||||
|
📜 锦囊内容:
|
||||||
|
🎯 PromptX 初始化完成!
|
||||||
|
|
||||||
|
## 📦 版本信息
|
||||||
|
✅ **PromptX v0.0.2-snapshot.20250614141120.2d90a70 (dpml-prompt@0.0.2-snapshot.20250614141120.2d90a70, Node.js v24.2.0)** - AI专业能
|
||||||
|
力增强框架
|
||||||
|
|
||||||
|
## 🏗️ 环境准备
|
||||||
|
✅ 创建了 `.promptx` 配置目录
|
||||||
|
✅ 工作环境就绪
|
||||||
|
|
||||||
|
📂 目录: ..\..\.promptx\resource\domain
|
||||||
|
💾 注册表: ..\..\.promptx\resource\project.registry.json
|
||||||
|
💡 现在可以在 domain 目录下创建角色资源了
|
||||||
|
|
||||||
|
## 🚀 下一步建议
|
||||||
|
- 使用 `hello` 发现可用的专业角色
|
||||||
|
- 使用 `action` 激活特定角色获得专业能力
|
||||||
|
- 使用 `learn` 深入学习专业知识
|
||||||
|
- 使用 `remember/recall` 管理专业记忆
|
||||||
|
|
||||||
|
💡 **提示**: 现在可以开始创建项目级资源了!
|
||||||
|
|
||||||
|
🔄 下一步行动:
|
||||||
|
- 发现专业角色: 查看所有可用的AI专业角色
|
||||||
|
方式: npx dpml-prompt@snapshot hello
|
||||||
|
- 激活专业角色: 直接激活特定专业角色(如果已知角色ID)
|
||||||
|
方式: npx dpml-prompt@snapshot action
|
||||||
|
|
||||||
|
📍 当前状态:initialized
|
||||||
|
============================================================
|
||||||
|
|
||||||
|
PS C:\Users\Administrator\Desktop\LUCKY> cd
|
||||||
|
PS C:\Users\Administrator\Desktop\LUCKY> ls
|
||||||
|
|
||||||
|
|
||||||
|
目录: C:\Users\Administrator\Desktop\LUCKY
|
||||||
|
|
||||||
|
|
||||||
|
Mode LastWriteTime Length Name
|
||||||
|
---- ------------- ------ ----
|
||||||
|
d----- 2025/6/15/周日 10:19 .promptx
|
||||||
|
d----- 2025/6/12/周四 17:26 images
|
||||||
|
-a---- 2025/6/12/周四 17:30 3550 CREATIVE_INVENTORY_README.md
|
||||||
|
-a---- 2025/6/9/周一 6:23 72483 drops.txt
|
||||||
|
-a---- 2025/6/12/周四 19:23 53655 index.html
|
||||||
|
-a---- 2025/6/11/周三 12:22 1 main.js
|
||||||
|
-a---- 2025/6/12/周四 16:51 7392 mod_entities.txt
|
||||||
|
-a---- 2025/6/12/周四 16:40 68867 mod_items.txt
|
||||||
|
-a---- 2025/6/12/周四 14:58 4512 README.md
|
||||||
|
-a---- 2025/6/12/周四 19:23 88986 script.js
|
||||||
|
-a---- 2025/6/12/周四 3:52 6113 styles.css
|
||||||
|
-a---- 2025/6/9/周一 5:16 26 测试.bat
|
||||||
|
-a---- 2025/6/12/周四 19:52 6192 清理Cursor缓存.ps1
|
||||||
|
|
||||||
|
|
||||||
|
PS C:\Users\Administrator\Desktop\LUCKY>
|
||||||
@ -1,7 +1,7 @@
|
|||||||
const BasePouchCommand = require('../BasePouchCommand')
|
const BasePouchCommand = require('../BasePouchCommand')
|
||||||
const { getGlobalResourceManager } = require('../../resource')
|
const { getGlobalResourceManager } = require('../../resource')
|
||||||
const { COMMANDS } = require('../../../../constants')
|
const { COMMANDS } = require('../../../../constants')
|
||||||
const PromptXConfig = require('../../../utils/promptxConfig')
|
const { getDirectoryService } = require('../../../utils/DirectoryService')
|
||||||
const RegistryData = require('../../resource/RegistryData')
|
const RegistryData = require('../../resource/RegistryData')
|
||||||
const ProjectDiscovery = require('../../resource/discovery/ProjectDiscovery')
|
const ProjectDiscovery = require('../../resource/discovery/ProjectDiscovery')
|
||||||
const logger = require('../../../utils/logger')
|
const logger = require('../../../utils/logger')
|
||||||
@ -18,6 +18,7 @@ class InitCommand extends BasePouchCommand {
|
|||||||
// 使用全局单例 ResourceManager
|
// 使用全局单例 ResourceManager
|
||||||
this.resourceManager = getGlobalResourceManager()
|
this.resourceManager = getGlobalResourceManager()
|
||||||
this.projectDiscovery = new ProjectDiscovery()
|
this.projectDiscovery = new ProjectDiscovery()
|
||||||
|
this.directoryService = getDirectoryService()
|
||||||
}
|
}
|
||||||
|
|
||||||
getPurpose () {
|
getPurpose () {
|
||||||
@ -27,14 +28,27 @@ class InitCommand extends BasePouchCommand {
|
|||||||
async getContent (args) {
|
async getContent (args) {
|
||||||
const [workspacePath = '.'] = args
|
const [workspacePath = '.'] = args
|
||||||
|
|
||||||
|
// 构建统一的查找上下文
|
||||||
|
// 对于init命令,我们优先使用当前目录,不向上查找现有.promptx
|
||||||
|
const context = {
|
||||||
|
startDir: workspacePath === '.' ? process.cwd() : path.resolve(workspacePath),
|
||||||
|
platform: process.platform,
|
||||||
|
avoidUserHome: true, // 特别是Windows环境下避免用户家目录
|
||||||
|
// init命令特有:优先当前目录,不查找现有.promptx
|
||||||
|
strategies: [
|
||||||
|
'currentWorkingDirectoryIfHasMarkers',
|
||||||
|
'currentWorkingDirectory' // 如果当前目录没有项目标识,就直接使用当前目录
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
// 1. 获取版本信息
|
// 1. 获取版本信息
|
||||||
const version = await this.getVersionInfo()
|
const version = await this.getVersionInfo()
|
||||||
|
|
||||||
// 2. 基础环境准备 - 只创建 .promptx 目录
|
// 2. 基础环境准备 - 创建 .promptx 目录
|
||||||
await this.ensurePromptXDirectory(workspacePath)
|
await this.ensurePromptXDirectory(context)
|
||||||
|
|
||||||
// 3. 生成项目级资源注册表
|
// 3. 生成项目级资源注册表
|
||||||
const registryStats = await this.generateProjectRegistry(workspacePath)
|
const registryStats = await this.generateProjectRegistry(context)
|
||||||
|
|
||||||
// 4. 刷新全局 ResourceManager(确保新资源立即可用)
|
// 4. 刷新全局 ResourceManager(确保新资源立即可用)
|
||||||
await this.refreshGlobalResourceManager()
|
await this.refreshGlobalResourceManager()
|
||||||
@ -62,18 +76,17 @@ ${registryStats.message}
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成项目级资源注册表
|
* 生成项目级资源注册表
|
||||||
* @param {string} workspacePath - 工作目录路径
|
* @param {Object} context - 查找上下文
|
||||||
* @returns {Promise<Object>} 注册表生成统计信息
|
* @returns {Promise<Object>} 注册表生成统计信息
|
||||||
*/
|
*/
|
||||||
async generateProjectRegistry(workspacePath) {
|
async generateProjectRegistry(context) {
|
||||||
try {
|
try {
|
||||||
// 1. 获取项目根目录
|
// 1. 使用统一的目录服务获取项目根目录
|
||||||
const projectRoot = await this.projectDiscovery._findProjectRoot()
|
const projectRoot = await this.directoryService.getProjectRoot(context)
|
||||||
|
const resourceDir = await this.directoryService.getResourceDirectory(context)
|
||||||
// 2. 确保 .promptx/resource/domain 目录结构存在
|
|
||||||
const resourceDir = path.join(projectRoot, '.promptx', 'resource')
|
|
||||||
const domainDir = path.join(resourceDir, 'domain')
|
const domainDir = path.join(resourceDir, 'domain')
|
||||||
|
|
||||||
|
// 2. 确保目录结构存在
|
||||||
await fs.ensureDir(domainDir)
|
await fs.ensureDir(domainDir)
|
||||||
logger.debug(`[InitCommand] 确保目录结构存在: ${domainDir}`)
|
logger.debug(`[InitCommand] 确保目录结构存在: ${domainDir}`)
|
||||||
|
|
||||||
@ -83,7 +96,7 @@ ${registryStats.message}
|
|||||||
|
|
||||||
// 4. 生成统计信息
|
// 4. 生成统计信息
|
||||||
const stats = registryData.getStats()
|
const stats = registryData.getStats()
|
||||||
const registryPath = path.join(projectRoot, '.promptx', 'resource', 'project.registry.json')
|
const registryPath = await this.directoryService.getRegistryPath(context)
|
||||||
|
|
||||||
if (registryData.size === 0) {
|
if (registryData.size === 0) {
|
||||||
return {
|
return {
|
||||||
@ -114,12 +127,12 @@ ${registryStats.message}
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 确保 .promptx 基础目录存在
|
* 确保 .promptx 基础目录存在
|
||||||
* 这是 init 的唯一职责 - 创建基础环境标识
|
* 使用统一的目录服务创建基础环境
|
||||||
*/
|
*/
|
||||||
async ensurePromptXDirectory (workspacePath) {
|
async ensurePromptXDirectory (context) {
|
||||||
const config = new PromptXConfig(workspacePath)
|
const promptxDir = await this.directoryService.getPromptXDirectory(context)
|
||||||
// 利用 PromptXConfig 的统一目录管理
|
await fs.ensureDir(promptxDir)
|
||||||
await config.ensureDir()
|
logger.debug(`[InitCommand] 确保.promptx目录存在: ${promptxDir}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -89,7 +89,10 @@ ${formattedMemories}
|
|||||||
const memories = []
|
const memories = []
|
||||||
|
|
||||||
// 读取单一记忆文件
|
// 读取单一记忆文件
|
||||||
const memoryFile = path.join(process.cwd(), '.promptx/memory/declarative.md')
|
const { getDirectoryService } = require('../../../utils/DirectoryService')
|
||||||
|
const directoryService = getDirectoryService()
|
||||||
|
const memoryDir = await directoryService.getMemoryDirectory()
|
||||||
|
const memoryFile = path.join(memoryDir, 'declarative.md')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (await fs.pathExists(memoryFile)) {
|
if (await fs.pathExists(memoryFile)) {
|
||||||
|
|||||||
@ -70,9 +70,10 @@ class RememberCommand extends BasePouchCommand {
|
|||||||
* 确保AI记忆体系目录存在
|
* 确保AI记忆体系目录存在
|
||||||
*/
|
*/
|
||||||
async ensureMemoryDirectory () {
|
async ensureMemoryDirectory () {
|
||||||
const promptxDir = path.join(process.cwd(), '.promptx')
|
const { getDirectoryService } = require('../../../utils/DirectoryService')
|
||||||
const memoryDir = path.join(promptxDir, 'memory')
|
const directoryService = getDirectoryService()
|
||||||
|
|
||||||
|
const memoryDir = await directoryService.getMemoryDirectory()
|
||||||
await fs.ensureDir(memoryDir)
|
await fs.ensureDir(memoryDir)
|
||||||
|
|
||||||
return memoryDir
|
return memoryDir
|
||||||
|
|||||||
@ -107,7 +107,9 @@ class PouchStateMachine {
|
|||||||
* 保存状态到文件
|
* 保存状态到文件
|
||||||
*/
|
*/
|
||||||
async saveState () {
|
async saveState () {
|
||||||
const promptxDir = path.join(process.cwd(), '.promptx')
|
const { getDirectoryService } = require('../../../utils/DirectoryService')
|
||||||
|
const directoryService = getDirectoryService()
|
||||||
|
const promptxDir = await directoryService.getPromptXDirectory()
|
||||||
const configPath = path.join(promptxDir, 'pouch.json')
|
const configPath = path.join(promptxDir, 'pouch.json')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -133,7 +135,10 @@ class PouchStateMachine {
|
|||||||
* 从文件加载状态
|
* 从文件加载状态
|
||||||
*/
|
*/
|
||||||
async loadState () {
|
async loadState () {
|
||||||
const configPath = path.join(process.cwd(), '.promptx', 'pouch.json')
|
const { getDirectoryService } = require('../../../utils/DirectoryService')
|
||||||
|
const directoryService = getDirectoryService()
|
||||||
|
const promptxDir = await directoryService.getPromptXDirectory()
|
||||||
|
const configPath = path.join(promptxDir, 'pouch.json')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (await fs.pathExists(configPath)) {
|
if (await fs.pathExists(configPath)) {
|
||||||
|
|||||||
@ -128,33 +128,15 @@ class ProjectDiscovery extends BaseDiscovery {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 查找项目根目录
|
* 查找项目根目录
|
||||||
|
* @deprecated 使用 DirectoryService.getProjectRoot() 替代
|
||||||
* @returns {Promise<string>} 项目根目录路径
|
* @returns {Promise<string>} 项目根目录路径
|
||||||
*/
|
*/
|
||||||
async _findProjectRoot() {
|
async _findProjectRoot() {
|
||||||
const cacheKey = 'projectRoot'
|
// 使用新的统一目录服务
|
||||||
const cached = this.getFromCache(cacheKey)
|
const { getDirectoryService } = require('../../../utils/DirectoryService')
|
||||||
if (cached) {
|
const directoryService = getDirectoryService()
|
||||||
return cached
|
|
||||||
}
|
|
||||||
|
|
||||||
let currentDir = process.cwd()
|
return await directoryService.getProjectRoot()
|
||||||
|
|
||||||
// 向上查找包含package.json的目录
|
|
||||||
while (currentDir !== path.dirname(currentDir)) {
|
|
||||||
const packageJsonPath = path.join(currentDir, 'package.json')
|
|
||||||
|
|
||||||
if (await this._fsExists(packageJsonPath)) {
|
|
||||||
this.setCache(cacheKey, currentDir)
|
|
||||||
return currentDir
|
|
||||||
}
|
|
||||||
|
|
||||||
currentDir = path.dirname(currentDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果没找到package.json,返回当前工作目录
|
|
||||||
const fallbackRoot = process.cwd()
|
|
||||||
this.setCache(cacheKey, fallbackRoot)
|
|
||||||
return fallbackRoot
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
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
|
||||||
|
}
|
||||||
237
src/lib/utils/DirectoryService.js
Normal file
237
src/lib/utils/DirectoryService.js
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
const { DirectoryLocatorFactory } = require('./DirectoryLocator')
|
||||||
|
const logger = require('./logger')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 全局目录服务
|
||||||
|
* 为整个应用提供统一的路径解析服务
|
||||||
|
* 单例模式,确保全局一致性
|
||||||
|
*/
|
||||||
|
class DirectoryService {
|
||||||
|
constructor() {
|
||||||
|
this.projectRootLocator = null
|
||||||
|
this.workspaceLocator = null
|
||||||
|
this.initialized = false
|
||||||
|
|
||||||
|
// 缓存最后的结果,避免重复计算
|
||||||
|
this._lastProjectRoot = null
|
||||||
|
this._lastWorkspace = null
|
||||||
|
this._lastContext = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化服务
|
||||||
|
*/
|
||||||
|
async initialize(options = {}) {
|
||||||
|
if (this.initialized) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.projectRootLocator = DirectoryLocatorFactory.createProjectRootLocator(options)
|
||||||
|
this.workspaceLocator = DirectoryLocatorFactory.createPromptXWorkspaceLocator(options)
|
||||||
|
this.initialized = true
|
||||||
|
|
||||||
|
logger.debug('[DirectoryService] 初始化完成')
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[DirectoryService] 初始化失败:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取项目根目录
|
||||||
|
* @param {Object} context - 查找上下文
|
||||||
|
* @returns {Promise<string>} 项目根目录路径
|
||||||
|
*/
|
||||||
|
async getProjectRoot(context = {}) {
|
||||||
|
await this._ensureInitialized()
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await this.projectRootLocator.locate(context)
|
||||||
|
this._lastProjectRoot = result
|
||||||
|
this._lastContext = context
|
||||||
|
|
||||||
|
logger.debug(`[DirectoryService] 项目根目录: ${result}`)
|
||||||
|
return result
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[DirectoryService] 获取项目根目录失败:', error)
|
||||||
|
// 回退到当前目录
|
||||||
|
return context.startDir || process.cwd()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取PromptX工作空间目录
|
||||||
|
* @param {Object} context - 查找上下文
|
||||||
|
* @returns {Promise<string>} 工作空间目录路径
|
||||||
|
*/
|
||||||
|
async getWorkspace(context = {}) {
|
||||||
|
await this._ensureInitialized()
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await this.workspaceLocator.locate(context)
|
||||||
|
this._lastWorkspace = result
|
||||||
|
this._lastContext = context
|
||||||
|
|
||||||
|
logger.debug(`[DirectoryService] 工作空间目录: ${result}`)
|
||||||
|
return result
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[DirectoryService] 获取工作空间目录失败:', error)
|
||||||
|
// 回退到项目根目录
|
||||||
|
return await this.getProjectRoot(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取.promptx目录路径
|
||||||
|
* @param {Object} context - 查找上下文
|
||||||
|
* @returns {Promise<string>} .promptx目录路径
|
||||||
|
*/
|
||||||
|
async getPromptXDirectory(context = {}) {
|
||||||
|
const workspace = await this.getWorkspace(context)
|
||||||
|
return require('path').join(workspace, '.promptx')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取项目资源目录路径
|
||||||
|
* @param {Object} context - 查找上下文
|
||||||
|
* @returns {Promise<string>} 项目资源目录路径
|
||||||
|
*/
|
||||||
|
async getResourceDirectory(context = {}) {
|
||||||
|
const promptxDir = await this.getPromptXDirectory(context)
|
||||||
|
return require('path').join(promptxDir, 'resource')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取项目注册表文件路径
|
||||||
|
* @param {Object} context - 查找上下文
|
||||||
|
* @returns {Promise<string>} 注册表文件路径
|
||||||
|
*/
|
||||||
|
async getRegistryPath(context = {}) {
|
||||||
|
const resourceDir = await this.getResourceDirectory(context)
|
||||||
|
return require('path').join(resourceDir, 'project.registry.json')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取记忆目录路径
|
||||||
|
* @param {Object} context - 查找上下文
|
||||||
|
* @returns {Promise<string>} 记忆目录路径
|
||||||
|
*/
|
||||||
|
async getMemoryDirectory(context = {}) {
|
||||||
|
const promptxDir = await this.getPromptXDirectory(context)
|
||||||
|
return require('path').join(promptxDir, 'memory')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除所有缓存
|
||||||
|
*/
|
||||||
|
clearCache() {
|
||||||
|
if (this.projectRootLocator) {
|
||||||
|
this.projectRootLocator.clearCache()
|
||||||
|
}
|
||||||
|
if (this.workspaceLocator) {
|
||||||
|
this.workspaceLocator.clearCache()
|
||||||
|
}
|
||||||
|
|
||||||
|
this._lastProjectRoot = null
|
||||||
|
this._lastWorkspace = null
|
||||||
|
this._lastContext = null
|
||||||
|
|
||||||
|
logger.debug('[DirectoryService] 缓存已清除')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取调试信息
|
||||||
|
*/
|
||||||
|
async getDebugInfo(context = {}) {
|
||||||
|
await this._ensureInitialized()
|
||||||
|
|
||||||
|
const projectRoot = await this.getProjectRoot(context)
|
||||||
|
const workspace = await this.getWorkspace(context)
|
||||||
|
const promptxDir = await this.getPromptXDirectory(context)
|
||||||
|
|
||||||
|
return {
|
||||||
|
platform: process.platform,
|
||||||
|
projectRoot,
|
||||||
|
workspace,
|
||||||
|
promptxDirectory: promptxDir,
|
||||||
|
isSame: projectRoot === workspace,
|
||||||
|
environment: {
|
||||||
|
WORKSPACE_FOLDER_PATHS: process.env.WORKSPACE_FOLDER_PATHS,
|
||||||
|
PROMPTX_WORKSPACE: process.env.PROMPTX_WORKSPACE,
|
||||||
|
PWD: process.env.PWD,
|
||||||
|
NODE_ENV: process.env.NODE_ENV
|
||||||
|
},
|
||||||
|
context,
|
||||||
|
cache: {
|
||||||
|
projectRootCacheSize: this.projectRootLocator?.cache.size || 0,
|
||||||
|
workspaceCacheSize: this.workspaceLocator?.cache.size || 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确保服务已初始化
|
||||||
|
*/
|
||||||
|
async _ensureInitialized() {
|
||||||
|
if (!this.initialized) {
|
||||||
|
await this.initialize()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重新加载配置
|
||||||
|
* @param {Object} options - 新的配置选项
|
||||||
|
*/
|
||||||
|
async reload(options = {}) {
|
||||||
|
this.initialized = false
|
||||||
|
this.clearCache()
|
||||||
|
await this.initialize(options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建全局单例
|
||||||
|
const globalDirectoryService = new DirectoryService()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取全局目录服务实例
|
||||||
|
* @returns {DirectoryService} 目录服务实例
|
||||||
|
*/
|
||||||
|
function getDirectoryService() {
|
||||||
|
return globalDirectoryService
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 便捷方法:获取项目根目录
|
||||||
|
* @param {Object} context - 查找上下文
|
||||||
|
* @returns {Promise<string>} 项目根目录路径
|
||||||
|
*/
|
||||||
|
async function getProjectRoot(context = {}) {
|
||||||
|
return await globalDirectoryService.getProjectRoot(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 便捷方法:获取工作空间目录
|
||||||
|
* @param {Object} context - 查找上下文
|
||||||
|
* @returns {Promise<string>} 工作空间目录路径
|
||||||
|
*/
|
||||||
|
async function getWorkspace(context = {}) {
|
||||||
|
return await globalDirectoryService.getWorkspace(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 便捷方法:获取.promptx目录
|
||||||
|
* @param {Object} context - 查找上下文
|
||||||
|
* @returns {Promise<string>} .promptx目录路径
|
||||||
|
*/
|
||||||
|
async function getPromptXDirectory(context = {}) {
|
||||||
|
return await globalDirectoryService.getPromptXDirectory(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
DirectoryService,
|
||||||
|
getDirectoryService,
|
||||||
|
getProjectRoot,
|
||||||
|
getWorkspace,
|
||||||
|
getPromptXDirectory
|
||||||
|
}
|
||||||
@ -2,11 +2,15 @@ const fs = require('fs');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const os = require('os');
|
const os = require('os');
|
||||||
const logger = require('./logger');
|
const logger = require('./logger');
|
||||||
|
const { getDirectoryService } = require('./DirectoryService');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 执行上下文检测工具
|
* 执行上下文检测工具 (已重构)
|
||||||
* 根据命令入口自动判断执行模式(CLI vs MCP)并获取正确的工作目录
|
*
|
||||||
* 基于MCP社区标准实践,通过环境变量解决cwd获取问题
|
* 现在使用统一的DirectoryService提供路径解析
|
||||||
|
* 保持向后兼容的API,但内部使用新的架构
|
||||||
|
*
|
||||||
|
* @deprecated 推荐直接使用 DirectoryService
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -29,22 +33,61 @@ function getExecutionContext() {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* MCP模式下获取工作目录
|
* MCP模式下获取工作目录
|
||||||
* 基于社区标准实践,优先从环境变量获取配置的工作目录
|
* 使用新的DirectoryService进行路径解析
|
||||||
* @returns {string} 工作目录路径
|
* @returns {string} 工作目录路径
|
||||||
*/
|
*/
|
||||||
function getMCPWorkingDirectory() {
|
function getMCPWorkingDirectory() {
|
||||||
// 策略1:WORKSPACE_FOLDER_PATHS(VS Code/Cursor标准环境变量)
|
try {
|
||||||
|
const directoryService = getDirectoryService();
|
||||||
|
|
||||||
|
// 使用新的统一路径解析服务
|
||||||
|
// 注意:这是异步操作,但为了保持API兼容性,我们需要同步处理
|
||||||
|
// 在实际使用中,建议迁移到异步版本
|
||||||
|
const context = {
|
||||||
|
startDir: process.cwd(),
|
||||||
|
platform: process.platform,
|
||||||
|
avoidUserHome: true
|
||||||
|
};
|
||||||
|
|
||||||
|
// 同步获取工作空间目录
|
||||||
|
// TODO: 在后续版本中迁移到异步API
|
||||||
|
return getWorkspaceSynchronous(context);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('[executionContext] 使用新服务失败,回退到旧逻辑:', error.message);
|
||||||
|
return getMCPWorkingDirectoryLegacy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步获取工作空间(临时解决方案)
|
||||||
|
* @param {Object} context - 查找上下文
|
||||||
|
* @returns {string} 工作空间路径
|
||||||
|
*/
|
||||||
|
function getWorkspaceSynchronous(context) {
|
||||||
|
// 策略1:IDE环境变量
|
||||||
const workspacePaths = process.env.WORKSPACE_FOLDER_PATHS;
|
const workspacePaths = process.env.WORKSPACE_FOLDER_PATHS;
|
||||||
if (workspacePaths) {
|
if (workspacePaths) {
|
||||||
// 取第一个工作区路径(多工作区情况)
|
try {
|
||||||
const firstPath = workspacePaths.split(path.delimiter)[0];
|
const folders = JSON.parse(workspacePaths);
|
||||||
if (firstPath && isValidDirectory(firstPath)) {
|
if (Array.isArray(folders) && folders.length > 0) {
|
||||||
console.error(`[执行上下文] 使用WORKSPACE_FOLDER_PATHS: ${firstPath}`);
|
const firstFolder = folders[0];
|
||||||
return firstPath;
|
if (isValidDirectory(firstFolder)) {
|
||||||
|
console.error(`[执行上下文] 使用WORKSPACE_FOLDER_PATHS: ${firstFolder}`);
|
||||||
|
return firstFolder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 忽略解析错误,尝试直接使用
|
||||||
|
const firstPath = workspacePaths.split(path.delimiter)[0];
|
||||||
|
if (firstPath && isValidDirectory(firstPath)) {
|
||||||
|
console.error(`[执行上下文] 使用WORKSPACE_FOLDER_PATHS: ${firstPath}`);
|
||||||
|
return firstPath;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 策略2:PROMPTX_WORKSPACE(PromptX专用环境变量,仅当明确配置且非空时使用)
|
// 策略2:PromptX专用环境变量
|
||||||
const promptxWorkspaceEnv = process.env.PROMPTX_WORKSPACE;
|
const promptxWorkspaceEnv = process.env.PROMPTX_WORKSPACE;
|
||||||
if (promptxWorkspaceEnv && promptxWorkspaceEnv.trim() !== '') {
|
if (promptxWorkspaceEnv && promptxWorkspaceEnv.trim() !== '') {
|
||||||
const promptxWorkspace = normalizePath(expandHome(promptxWorkspaceEnv));
|
const promptxWorkspace = normalizePath(expandHome(promptxWorkspaceEnv));
|
||||||
@ -54,30 +97,39 @@ function getMCPWorkingDirectory() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 策略3:向上查找现有.promptx目录(复用现有项目配置)
|
// 策略3:现有.promptx目录
|
||||||
const existingPrompxRoot = findExistingPromptxDirectory(process.cwd());
|
const existingPrompxRoot = findExistingPromptxDirectory(context.startDir);
|
||||||
if (existingPrompxRoot) {
|
if (existingPrompxRoot) {
|
||||||
console.error(`[执行上下文] 发现现有.promptx目录: ${existingPrompxRoot}`);
|
console.error(`[执行上下文] 发现现有.promptx目录: ${existingPrompxRoot}`);
|
||||||
return existingPrompxRoot;
|
return existingPrompxRoot;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 策略4:PWD环境变量(某些情况下可用)
|
// 策略4:PWD环境变量
|
||||||
const pwd = process.env.PWD;
|
const pwd = process.env.PWD;
|
||||||
if (pwd && isValidDirectory(pwd) && pwd !== process.cwd()) {
|
if (pwd && isValidDirectory(pwd) && pwd !== process.cwd()) {
|
||||||
console.error(`[执行上下文] 使用PWD环境变量: ${pwd}`);
|
console.error(`[执行上下文] 使用PWD环境变量: ${pwd}`);
|
||||||
return pwd;
|
return pwd;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 策略5:项目根目录智能推测(向上查找项目标识)
|
// 策略5:项目根目录
|
||||||
const projectRoot = findProjectRoot(process.cwd());
|
const projectRoot = findProjectRoot(context.startDir);
|
||||||
if (projectRoot && projectRoot !== process.cwd()) {
|
if (projectRoot && projectRoot !== process.cwd()) {
|
||||||
console.error(`[执行上下文] 智能推测项目根目录: ${projectRoot}`);
|
console.error(`[执行上下文] 智能推测项目根目录: ${projectRoot}`);
|
||||||
return projectRoot;
|
return projectRoot;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 策略6:回退到process.cwd()
|
// 策略6:回退到当前目录
|
||||||
console.error(`[执行上下文] 回退到process.cwd(): ${process.cwd()}`);
|
console.error(`[执行上下文] 回退到process.cwd(): ${process.cwd()}`);
|
||||||
console.error(`[执行上下文] 提示:建议在MCP配置中添加 "env": {"PROMPTX_WORKSPACE": "你的项目目录"}`)
|
console.error(`[执行上下文] 提示:建议在MCP配置中添加 "env": {"PROMPTX_WORKSPACE": "你的项目目录"}`);
|
||||||
|
return process.cwd();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 旧版MCP工作目录获取逻辑(兼容性备用)
|
||||||
|
* @deprecated
|
||||||
|
*/
|
||||||
|
function getMCPWorkingDirectoryLegacy() {
|
||||||
|
// 保留原始的同步逻辑作为备份
|
||||||
return process.cwd();
|
return process.cwd();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,6 +186,18 @@ function findProjectRoot(startDir) {
|
|||||||
const root = path.parse(currentDir).root;
|
const root = path.parse(currentDir).root;
|
||||||
|
|
||||||
while (currentDir !== root) {
|
while (currentDir !== root) {
|
||||||
|
// Windows特有:避免用户家目录
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
const homeDir = os.homedir();
|
||||||
|
if (path.resolve(currentDir) === path.resolve(homeDir)) {
|
||||||
|
console.error(`[executionContext] 跳过用户家目录: ${currentDir}`);
|
||||||
|
const parentDir = path.dirname(currentDir);
|
||||||
|
if (parentDir === currentDir) break;
|
||||||
|
currentDir = parentDir;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 检查是否包含项目标识文件
|
// 检查是否包含项目标识文件
|
||||||
for (const marker of projectMarkers) {
|
for (const marker of projectMarkers) {
|
||||||
const markerPath = path.join(currentDir, marker);
|
const markerPath = path.join(currentDir, marker);
|
||||||
@ -193,15 +257,25 @@ function getDebugInfo() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 规范化路径
|
||||||
|
*/
|
||||||
function normalizePath(p) {
|
function normalizePath(p) {
|
||||||
return path.normalize(p);
|
return path.normalize(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 展开家目录路径
|
||||||
|
*/
|
||||||
function expandHome(filepath) {
|
function expandHome(filepath) {
|
||||||
if (filepath.startsWith('~/') || filepath === '~') {
|
if (!filepath || typeof filepath !== 'string') {
|
||||||
return path.join(os.homedir(), filepath.slice(1));
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (filepath.startsWith('~/') || filepath === '~') {
|
||||||
|
return path.join(os.homedir(), filepath.slice(2));
|
||||||
|
}
|
||||||
|
|
||||||
return filepath;
|
return filepath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
282
src/tests/utils/DirectoryService.integration.test.js
Normal file
282
src/tests/utils/DirectoryService.integration.test.js
Normal file
@ -0,0 +1,282 @@
|
|||||||
|
const path = require('path')
|
||||||
|
const fs = require('fs-extra')
|
||||||
|
const os = require('os')
|
||||||
|
const {
|
||||||
|
DirectoryService,
|
||||||
|
getDirectoryService,
|
||||||
|
getProjectRoot,
|
||||||
|
getWorkspace,
|
||||||
|
getPromptXDirectory
|
||||||
|
} = require('../../lib/utils/DirectoryService')
|
||||||
|
|
||||||
|
describe('DirectoryService 集成测试', () => {
|
||||||
|
let tempDir
|
||||||
|
let originalCwd
|
||||||
|
let originalEnv
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
originalCwd = process.cwd()
|
||||||
|
originalEnv = { ...process.env }
|
||||||
|
|
||||||
|
// 创建临时测试目录
|
||||||
|
const tempBase = os.tmpdir()
|
||||||
|
tempDir = path.join(tempBase, `promptx-test-${Date.now()}`)
|
||||||
|
await fs.ensureDir(tempDir)
|
||||||
|
|
||||||
|
// 清除环境变量
|
||||||
|
delete process.env.WORKSPACE_FOLDER_PATHS
|
||||||
|
delete process.env.PROMPTX_WORKSPACE
|
||||||
|
delete process.env.PWD
|
||||||
|
|
||||||
|
// 清除服务缓存
|
||||||
|
const service = getDirectoryService()
|
||||||
|
service.clearCache()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
process.chdir(originalCwd)
|
||||||
|
process.env = originalEnv
|
||||||
|
|
||||||
|
// 清理临时目录
|
||||||
|
if (tempDir && await fs.pathExists(tempDir)) {
|
||||||
|
await fs.remove(tempDir)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Windows用户家目录问题修复', () => {
|
||||||
|
// 跳过Windows特定测试如果不在Windows环境
|
||||||
|
const skipIfNotWindows = process.platform !== 'win32' ? test.skip : test
|
||||||
|
|
||||||
|
skipIfNotWindows('应该避免在用户家目录创建.promptx', async () => {
|
||||||
|
// 模拟用户在家目录的某个子目录下工作
|
||||||
|
const userHome = os.homedir()
|
||||||
|
const desktopDir = path.join(userHome, 'Desktop', 'LUCKY')
|
||||||
|
await fs.ensureDir(desktopDir)
|
||||||
|
|
||||||
|
// 在家目录创建package.json(模拟用户场景)
|
||||||
|
const homePackageJson = path.join(userHome, 'package.json')
|
||||||
|
await fs.writeJSON(homePackageJson, { name: 'user-home-project' })
|
||||||
|
|
||||||
|
// 在桌面目录创建一个真正的项目
|
||||||
|
const projectPackageJson = path.join(desktopDir, 'package.json')
|
||||||
|
await fs.writeJSON(projectPackageJson, { name: 'lucky-project' })
|
||||||
|
|
||||||
|
process.chdir(desktopDir)
|
||||||
|
|
||||||
|
const context = {
|
||||||
|
startDir: desktopDir,
|
||||||
|
platform: 'win32',
|
||||||
|
avoidUserHome: true
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectRoot = await getProjectRoot(context)
|
||||||
|
const workspace = await getWorkspace(context)
|
||||||
|
|
||||||
|
// 验证不会选择用户家目录
|
||||||
|
expect(projectRoot).not.toBe(userHome)
|
||||||
|
expect(workspace).not.toBe(userHome)
|
||||||
|
|
||||||
|
// 应该选择桌面目录下的项目
|
||||||
|
expect(projectRoot).toBe(desktopDir)
|
||||||
|
expect(workspace).toBe(desktopDir)
|
||||||
|
|
||||||
|
// 清理
|
||||||
|
await fs.remove(homePackageJson)
|
||||||
|
await fs.remove(desktopDir)
|
||||||
|
})
|
||||||
|
|
||||||
|
skipIfNotWindows('应该正确处理没有package.json的情况', async () => {
|
||||||
|
const testDir = path.join(tempDir, 'no-package')
|
||||||
|
await fs.ensureDir(testDir)
|
||||||
|
process.chdir(testDir)
|
||||||
|
|
||||||
|
const context = {
|
||||||
|
startDir: testDir,
|
||||||
|
platform: 'win32',
|
||||||
|
avoidUserHome: true
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectRoot = await getProjectRoot(context)
|
||||||
|
const workspace = await getWorkspace(context)
|
||||||
|
|
||||||
|
// 应该回退到当前目录而不是用户家目录
|
||||||
|
expect(projectRoot).toBe(testDir)
|
||||||
|
expect(workspace).toBe(testDir)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('环境变量优先级测试', () => {
|
||||||
|
test('WORKSPACE_FOLDER_PATHS应该有最高优先级', async () => {
|
||||||
|
const workspaceDir = path.join(tempDir, 'ide-workspace')
|
||||||
|
await fs.ensureDir(workspaceDir)
|
||||||
|
|
||||||
|
// 设置IDE环境变量
|
||||||
|
process.env.WORKSPACE_FOLDER_PATHS = JSON.stringify([workspaceDir])
|
||||||
|
|
||||||
|
const workspace = await getWorkspace()
|
||||||
|
expect(workspace).toBe(workspaceDir)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('PROMPTX_WORKSPACE应该作为备选', async () => {
|
||||||
|
const promptxWorkspace = path.join(tempDir, 'promptx-workspace')
|
||||||
|
await fs.ensureDir(promptxWorkspace)
|
||||||
|
|
||||||
|
process.env.PROMPTX_WORKSPACE = promptxWorkspace
|
||||||
|
|
||||||
|
const workspace = await getWorkspace()
|
||||||
|
expect(workspace).toBe(promptxWorkspace)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('现有.promptx目录应该被识别', async () => {
|
||||||
|
const projectDir = path.join(tempDir, 'existing-project')
|
||||||
|
const promptxDir = path.join(projectDir, '.promptx')
|
||||||
|
await fs.ensureDir(promptxDir)
|
||||||
|
|
||||||
|
const subDir = path.join(projectDir, 'subdir')
|
||||||
|
await fs.ensureDir(subDir)
|
||||||
|
process.chdir(subDir)
|
||||||
|
|
||||||
|
const context = { startDir: subDir }
|
||||||
|
const workspace = await getWorkspace(context)
|
||||||
|
|
||||||
|
expect(workspace).toBe(projectDir)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('统一路径解析验证', () => {
|
||||||
|
test('Init命令应该使用统一的路径逻辑', async () => {
|
||||||
|
const projectDir = path.join(tempDir, 'init-test')
|
||||||
|
await fs.ensureDir(projectDir)
|
||||||
|
await fs.writeJSON(path.join(projectDir, 'package.json'), { name: 'test-project' })
|
||||||
|
|
||||||
|
process.chdir(projectDir)
|
||||||
|
|
||||||
|
const context = {
|
||||||
|
startDir: projectDir,
|
||||||
|
platform: process.platform,
|
||||||
|
avoidUserHome: true
|
||||||
|
}
|
||||||
|
|
||||||
|
const service = getDirectoryService()
|
||||||
|
|
||||||
|
const projectRoot = await service.getProjectRoot(context)
|
||||||
|
const workspace = await service.getWorkspace(context)
|
||||||
|
const promptxDir = await service.getPromptXDirectory(context)
|
||||||
|
const resourceDir = await service.getResourceDirectory(context)
|
||||||
|
const registryPath = await service.getRegistryPath(context)
|
||||||
|
|
||||||
|
// 验证所有路径都基于同一个根目录
|
||||||
|
expect(projectRoot).toBe(projectDir)
|
||||||
|
expect(workspace).toBe(projectDir)
|
||||||
|
expect(promptxDir).toBe(path.join(projectDir, '.promptx'))
|
||||||
|
expect(resourceDir).toBe(path.join(projectDir, '.promptx', 'resource'))
|
||||||
|
expect(registryPath).toBe(path.join(projectDir, '.promptx', 'resource', 'project.registry.json'))
|
||||||
|
})
|
||||||
|
|
||||||
|
test('所有命令应该使用相同的路径解析', async () => {
|
||||||
|
const projectDir = path.join(tempDir, 'unified-test')
|
||||||
|
await fs.ensureDir(projectDir)
|
||||||
|
await fs.writeJSON(path.join(projectDir, 'package.json'), { name: 'unified-project' })
|
||||||
|
|
||||||
|
process.chdir(projectDir)
|
||||||
|
|
||||||
|
// 模拟不同命令使用相同的上下文
|
||||||
|
const context = {
|
||||||
|
startDir: projectDir,
|
||||||
|
platform: process.platform,
|
||||||
|
avoidUserHome: true
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectRoot1 = await getProjectRoot(context)
|
||||||
|
const workspace1 = await getWorkspace(context)
|
||||||
|
const promptxDir1 = await getPromptXDirectory(context)
|
||||||
|
|
||||||
|
// 第二次调用应该返回相同结果(缓存验证)
|
||||||
|
const projectRoot2 = await getProjectRoot(context)
|
||||||
|
const workspace2 = await getWorkspace(context)
|
||||||
|
const promptxDir2 = await getPromptXDirectory(context)
|
||||||
|
|
||||||
|
expect(projectRoot1).toBe(projectRoot2)
|
||||||
|
expect(workspace1).toBe(workspace2)
|
||||||
|
expect(promptxDir1).toBe(promptxDir2)
|
||||||
|
|
||||||
|
// 所有路径应该一致
|
||||||
|
expect(projectRoot1).toBe(projectDir)
|
||||||
|
expect(workspace1).toBe(projectDir)
|
||||||
|
expect(promptxDir1).toBe(path.join(projectDir, '.promptx'))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('缓存机制验证', () => {
|
||||||
|
test('缓存应该正常工作', async () => {
|
||||||
|
const projectDir = path.join(tempDir, 'cache-test')
|
||||||
|
await fs.ensureDir(projectDir)
|
||||||
|
await fs.writeJSON(path.join(projectDir, 'package.json'), { name: 'cache-project' })
|
||||||
|
|
||||||
|
process.chdir(projectDir)
|
||||||
|
|
||||||
|
const context = { startDir: projectDir }
|
||||||
|
|
||||||
|
// 第一次调用
|
||||||
|
const result1 = await getProjectRoot(context)
|
||||||
|
|
||||||
|
// 第二次调用应该返回相同结果(缓存验证)
|
||||||
|
const result2 = await getProjectRoot(context)
|
||||||
|
|
||||||
|
expect(result1).toBe(result2)
|
||||||
|
expect(result1).toBe(projectDir)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('缓存清除应该正常工作', async () => {
|
||||||
|
const service = getDirectoryService()
|
||||||
|
const projectDir = path.join(tempDir, 'clear-cache-test')
|
||||||
|
await fs.ensureDir(projectDir)
|
||||||
|
await fs.writeJSON(path.join(projectDir, 'package.json'), { name: 'clear-cache-project' })
|
||||||
|
|
||||||
|
const context = { startDir: projectDir }
|
||||||
|
|
||||||
|
// 填充缓存
|
||||||
|
const result1 = await service.getProjectRoot(context)
|
||||||
|
|
||||||
|
// 验证结果正确
|
||||||
|
expect(result1).toBe(projectDir)
|
||||||
|
|
||||||
|
// 清除缓存
|
||||||
|
service.clearCache()
|
||||||
|
|
||||||
|
// 再次调用应该仍然返回正确结果
|
||||||
|
const result2 = await service.getProjectRoot(context)
|
||||||
|
expect(result2).toBe(projectDir)
|
||||||
|
expect(result1).toBe(result2)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('调试信息验证', () => {
|
||||||
|
test('应该提供完整的调试信息', async () => {
|
||||||
|
const projectDir = path.join(tempDir, 'debug-test')
|
||||||
|
await fs.ensureDir(projectDir)
|
||||||
|
await fs.writeJSON(path.join(projectDir, 'package.json'), { name: 'debug-project' })
|
||||||
|
|
||||||
|
process.chdir(projectDir)
|
||||||
|
|
||||||
|
const service = getDirectoryService()
|
||||||
|
const context = { startDir: projectDir }
|
||||||
|
|
||||||
|
const debugInfo = await service.getDebugInfo(context)
|
||||||
|
|
||||||
|
expect(debugInfo).toHaveProperty('platform')
|
||||||
|
expect(debugInfo).toHaveProperty('projectRoot')
|
||||||
|
expect(debugInfo).toHaveProperty('workspace')
|
||||||
|
expect(debugInfo).toHaveProperty('promptxDirectory')
|
||||||
|
expect(debugInfo).toHaveProperty('isSame')
|
||||||
|
expect(debugInfo).toHaveProperty('environment')
|
||||||
|
expect(debugInfo).toHaveProperty('context')
|
||||||
|
expect(debugInfo).toHaveProperty('cache')
|
||||||
|
|
||||||
|
expect(debugInfo.projectRoot).toBe(projectDir)
|
||||||
|
expect(debugInfo.workspace).toBe(projectDir)
|
||||||
|
expect(debugInfo.promptxDirectory).toBe(path.join(projectDir, '.promptx'))
|
||||||
|
expect(debugInfo.isSame).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user