From 041ece9af16b1b4d04c7cf71f353b44c05ef6f5c Mon Sep 17 00:00:00 2001 From: sean Date: Sun, 15 Jun 2025 11:23:19 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=EF=BC=9A=E5=BC=95=E5=85=A5?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=E7=9A=84DirectoryService=E4=BB=A5=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E7=9B=AE=E5=BD=95=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在InitCommand、RecallCommand、RememberCommand和PouchStateMachine中替换了直接路径处理逻辑,改为使用DirectoryService进行目录解析。 - 更新了ProjectDiscovery以使用新的getProjectRoot方法,标记旧方法为已弃用。 - 在executionContext中重构了工作目录获取逻辑,增强了兼容性和可维护性。 - 确保了对用户主目录的避免处理,提升了目录定位的智能性和可靠性。 此改动旨在提升代码的可读性和一致性,同时为未来的扩展打下基础。 --- docs/issues/new 1.MD | 73 +++ src/lib/core/pouch/commands/InitCommand.js | 47 +- src/lib/core/pouch/commands/RecallCommand.js | 5 +- .../core/pouch/commands/RememberCommand.js | 7 +- src/lib/core/pouch/state/PouchStateMachine.js | 9 +- .../resource/discovery/ProjectDiscovery.js | 30 +- src/lib/utils/DirectoryLocator.js | 460 ++++++++++++++++++ src/lib/utils/DirectoryService.js | 237 +++++++++ src/lib/utils/executionContext.js | 116 ++++- .../DirectoryService.integration.test.js | 282 +++++++++++ 10 files changed, 1198 insertions(+), 68 deletions(-) create mode 100644 docs/issues/new 1.MD create mode 100644 src/lib/utils/DirectoryLocator.js create mode 100644 src/lib/utils/DirectoryService.js create mode 100644 src/tests/utils/DirectoryService.integration.test.js diff --git a/docs/issues/new 1.MD b/docs/issues/new 1.MD new file mode 100644 index 0000000..873c212 --- /dev/null +++ b/docs/issues/new 1.MD @@ -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> \ No newline at end of file diff --git a/src/lib/core/pouch/commands/InitCommand.js b/src/lib/core/pouch/commands/InitCommand.js index 6164f88..99ca9fa 100644 --- a/src/lib/core/pouch/commands/InitCommand.js +++ b/src/lib/core/pouch/commands/InitCommand.js @@ -1,7 +1,7 @@ const BasePouchCommand = require('../BasePouchCommand') const { getGlobalResourceManager } = require('../../resource') const { COMMANDS } = require('../../../../constants') -const PromptXConfig = require('../../../utils/promptxConfig') +const { getDirectoryService } = require('../../../utils/DirectoryService') const RegistryData = require('../../resource/RegistryData') const ProjectDiscovery = require('../../resource/discovery/ProjectDiscovery') const logger = require('../../../utils/logger') @@ -18,6 +18,7 @@ class InitCommand extends BasePouchCommand { // 使用全局单例 ResourceManager this.resourceManager = getGlobalResourceManager() this.projectDiscovery = new ProjectDiscovery() + this.directoryService = getDirectoryService() } getPurpose () { @@ -27,14 +28,27 @@ class InitCommand extends BasePouchCommand { async getContent (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. 获取版本信息 const version = await this.getVersionInfo() - // 2. 基础环境准备 - 只创建 .promptx 目录 - await this.ensurePromptXDirectory(workspacePath) + // 2. 基础环境准备 - 创建 .promptx 目录 + await this.ensurePromptXDirectory(context) // 3. 生成项目级资源注册表 - const registryStats = await this.generateProjectRegistry(workspacePath) + const registryStats = await this.generateProjectRegistry(context) // 4. 刷新全局 ResourceManager(确保新资源立即可用) await this.refreshGlobalResourceManager() @@ -62,18 +76,17 @@ ${registryStats.message} /** * 生成项目级资源注册表 - * @param {string} workspacePath - 工作目录路径 + * @param {Object} context - 查找上下文 * @returns {Promise} 注册表生成统计信息 */ - async generateProjectRegistry(workspacePath) { + async generateProjectRegistry(context) { try { - // 1. 获取项目根目录 - const projectRoot = await this.projectDiscovery._findProjectRoot() - - // 2. 确保 .promptx/resource/domain 目录结构存在 - const resourceDir = path.join(projectRoot, '.promptx', 'resource') + // 1. 使用统一的目录服务获取项目根目录 + const projectRoot = await this.directoryService.getProjectRoot(context) + const resourceDir = await this.directoryService.getResourceDirectory(context) const domainDir = path.join(resourceDir, 'domain') + // 2. 确保目录结构存在 await fs.ensureDir(domainDir) logger.debug(`[InitCommand] 确保目录结构存在: ${domainDir}`) @@ -83,7 +96,7 @@ ${registryStats.message} // 4. 生成统计信息 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) { return { @@ -114,12 +127,12 @@ ${registryStats.message} /** * 确保 .promptx 基础目录存在 - * 这是 init 的唯一职责 - 创建基础环境标识 + * 使用统一的目录服务创建基础环境 */ - async ensurePromptXDirectory (workspacePath) { - const config = new PromptXConfig(workspacePath) - // 利用 PromptXConfig 的统一目录管理 - await config.ensureDir() + async ensurePromptXDirectory (context) { + const promptxDir = await this.directoryService.getPromptXDirectory(context) + await fs.ensureDir(promptxDir) + logger.debug(`[InitCommand] 确保.promptx目录存在: ${promptxDir}`) } /** diff --git a/src/lib/core/pouch/commands/RecallCommand.js b/src/lib/core/pouch/commands/RecallCommand.js index eb5d614..eb68b13 100644 --- a/src/lib/core/pouch/commands/RecallCommand.js +++ b/src/lib/core/pouch/commands/RecallCommand.js @@ -89,7 +89,10 @@ ${formattedMemories} 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 { if (await fs.pathExists(memoryFile)) { diff --git a/src/lib/core/pouch/commands/RememberCommand.js b/src/lib/core/pouch/commands/RememberCommand.js index 6fa3078..0e6786d 100644 --- a/src/lib/core/pouch/commands/RememberCommand.js +++ b/src/lib/core/pouch/commands/RememberCommand.js @@ -70,9 +70,10 @@ class RememberCommand extends BasePouchCommand { * 确保AI记忆体系目录存在 */ async ensureMemoryDirectory () { - const promptxDir = path.join(process.cwd(), '.promptx') - const memoryDir = path.join(promptxDir, 'memory') - + const { getDirectoryService } = require('../../../utils/DirectoryService') + const directoryService = getDirectoryService() + + const memoryDir = await directoryService.getMemoryDirectory() await fs.ensureDir(memoryDir) return memoryDir diff --git a/src/lib/core/pouch/state/PouchStateMachine.js b/src/lib/core/pouch/state/PouchStateMachine.js index a44a0d0..8b80b81 100644 --- a/src/lib/core/pouch/state/PouchStateMachine.js +++ b/src/lib/core/pouch/state/PouchStateMachine.js @@ -107,7 +107,9 @@ class PouchStateMachine { * 保存状态到文件 */ 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') try { @@ -133,7 +135,10 @@ class PouchStateMachine { * 从文件加载状态 */ 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 { if (await fs.pathExists(configPath)) { diff --git a/src/lib/core/resource/discovery/ProjectDiscovery.js b/src/lib/core/resource/discovery/ProjectDiscovery.js index f5744c4..f7df3c0 100644 --- a/src/lib/core/resource/discovery/ProjectDiscovery.js +++ b/src/lib/core/resource/discovery/ProjectDiscovery.js @@ -128,33 +128,15 @@ class ProjectDiscovery extends BaseDiscovery { /** * 查找项目根目录 + * @deprecated 使用 DirectoryService.getProjectRoot() 替代 * @returns {Promise} 项目根目录路径 */ async _findProjectRoot() { - const cacheKey = 'projectRoot' - const cached = this.getFromCache(cacheKey) - if (cached) { - return cached - } - - let currentDir = process.cwd() - - // 向上查找包含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 + // 使用新的统一目录服务 + const { getDirectoryService } = require('../../../utils/DirectoryService') + const directoryService = getDirectoryService() + + return await directoryService.getProjectRoot() } /** diff --git a/src/lib/utils/DirectoryLocator.js b/src/lib/utils/DirectoryLocator.js new file mode 100644 index 0000000..093ef14 --- /dev/null +++ b/src/lib/utils/DirectoryLocator.js @@ -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} 定位到的目录路径 + */ + 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 +} \ No newline at end of file diff --git a/src/lib/utils/DirectoryService.js b/src/lib/utils/DirectoryService.js new file mode 100644 index 0000000..df22e5b --- /dev/null +++ b/src/lib/utils/DirectoryService.js @@ -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} 项目根目录路径 + */ + 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} 工作空间目录路径 + */ + 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} .promptx目录路径 + */ + async getPromptXDirectory(context = {}) { + const workspace = await this.getWorkspace(context) + return require('path').join(workspace, '.promptx') + } + + /** + * 获取项目资源目录路径 + * @param {Object} context - 查找上下文 + * @returns {Promise} 项目资源目录路径 + */ + async getResourceDirectory(context = {}) { + const promptxDir = await this.getPromptXDirectory(context) + return require('path').join(promptxDir, 'resource') + } + + /** + * 获取项目注册表文件路径 + * @param {Object} context - 查找上下文 + * @returns {Promise} 注册表文件路径 + */ + async getRegistryPath(context = {}) { + const resourceDir = await this.getResourceDirectory(context) + return require('path').join(resourceDir, 'project.registry.json') + } + + /** + * 获取记忆目录路径 + * @param {Object} context - 查找上下文 + * @returns {Promise} 记忆目录路径 + */ + 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} 项目根目录路径 + */ +async function getProjectRoot(context = {}) { + return await globalDirectoryService.getProjectRoot(context) +} + +/** + * 便捷方法:获取工作空间目录 + * @param {Object} context - 查找上下文 + * @returns {Promise} 工作空间目录路径 + */ +async function getWorkspace(context = {}) { + return await globalDirectoryService.getWorkspace(context) +} + +/** + * 便捷方法:获取.promptx目录 + * @param {Object} context - 查找上下文 + * @returns {Promise} .promptx目录路径 + */ +async function getPromptXDirectory(context = {}) { + return await globalDirectoryService.getPromptXDirectory(context) +} + +module.exports = { + DirectoryService, + getDirectoryService, + getProjectRoot, + getWorkspace, + getPromptXDirectory +} \ No newline at end of file diff --git a/src/lib/utils/executionContext.js b/src/lib/utils/executionContext.js index 2e04022..91b8a06 100644 --- a/src/lib/utils/executionContext.js +++ b/src/lib/utils/executionContext.js @@ -2,11 +2,15 @@ const fs = require('fs'); const path = require('path'); const os = require('os'); const logger = require('./logger'); +const { getDirectoryService } = require('./DirectoryService'); /** - * 执行上下文检测工具 - * 根据命令入口自动判断执行模式(CLI vs MCP)并获取正确的工作目录 - * 基于MCP社区标准实践,通过环境变量解决cwd获取问题 + * 执行上下文检测工具 (已重构) + * + * 现在使用统一的DirectoryService提供路径解析 + * 保持向后兼容的API,但内部使用新的架构 + * + * @deprecated 推荐直接使用 DirectoryService */ /** @@ -29,22 +33,61 @@ function getExecutionContext() { /** * MCP模式下获取工作目录 - * 基于社区标准实践,优先从环境变量获取配置的工作目录 + * 使用新的DirectoryService进行路径解析 * @returns {string} 工作目录路径 */ 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; if (workspacePaths) { - // 取第一个工作区路径(多工作区情况) - const firstPath = workspacePaths.split(path.delimiter)[0]; - if (firstPath && isValidDirectory(firstPath)) { - console.error(`[执行上下文] 使用WORKSPACE_FOLDER_PATHS: ${firstPath}`); - return firstPath; + try { + const folders = JSON.parse(workspacePaths); + if (Array.isArray(folders) && folders.length > 0) { + const firstFolder = folders[0]; + 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; if (promptxWorkspaceEnv && promptxWorkspaceEnv.trim() !== '') { const promptxWorkspace = normalizePath(expandHome(promptxWorkspaceEnv)); @@ -54,30 +97,39 @@ function getMCPWorkingDirectory() { } } - // 策略3:向上查找现有.promptx目录(复用现有项目配置) - const existingPrompxRoot = findExistingPromptxDirectory(process.cwd()); + // 策略3:现有.promptx目录 + const existingPrompxRoot = findExistingPromptxDirectory(context.startDir); if (existingPrompxRoot) { console.error(`[执行上下文] 发现现有.promptx目录: ${existingPrompxRoot}`); return existingPrompxRoot; } - // 策略4:PWD环境变量(某些情况下可用) + // 策略4:PWD环境变量 const pwd = process.env.PWD; if (pwd && isValidDirectory(pwd) && pwd !== process.cwd()) { console.error(`[执行上下文] 使用PWD环境变量: ${pwd}`); return pwd; } - // 策略5:项目根目录智能推测(向上查找项目标识) - const projectRoot = findProjectRoot(process.cwd()); + // 策略5:项目根目录 + const projectRoot = findProjectRoot(context.startDir); if (projectRoot && projectRoot !== process.cwd()) { console.error(`[执行上下文] 智能推测项目根目录: ${projectRoot}`); return projectRoot; } - // 策略6:回退到process.cwd() + // 策略6:回退到当前目录 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(); } @@ -134,6 +186,18 @@ function findProjectRoot(startDir) { const root = path.parse(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) { const markerPath = path.join(currentDir, marker); @@ -193,15 +257,25 @@ function getDebugInfo() { }; } - +/** + * 规范化路径 + */ function normalizePath(p) { return path.normalize(p); } +/** + * 展开家目录路径 + */ function expandHome(filepath) { - if (filepath.startsWith('~/') || filepath === '~') { - return path.join(os.homedir(), filepath.slice(1)); + if (!filepath || typeof filepath !== 'string') { + return ''; } + + if (filepath.startsWith('~/') || filepath === '~') { + return path.join(os.homedir(), filepath.slice(2)); + } + return filepath; } diff --git a/src/tests/utils/DirectoryService.integration.test.js b/src/tests/utils/DirectoryService.integration.test.js new file mode 100644 index 0000000..6b8aa6b --- /dev/null +++ b/src/tests/utils/DirectoryService.integration.test.js @@ -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) + }) + }) +}) \ No newline at end of file