{"record":{"id":"3c72e2ddde282bc4","repo":"bmad-code-org/BMAD-METHOD","slug":"local-paths-do-not-support-version-suffixes","errorCode":null,"errorMessage":"Local paths do not support @version suffixes","messagePattern":"Local paths do not support @version suffixes","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"tools/installer/modules/custom-module-manager.js","lineNumber":331,"sourceCode":"\n    if (!Array.isArray(plugins) || plugins.length === 0) {\n      throw new Error('marketplace.json contains no plugins');\n    }\n\n    return plugins.map((plugin) => this._normalizeCustomModule(plugin, sourceUrl, marketplaceData));\n  }\n\n  // ─── Source Resolution ────────────────────────────────────────────────────\n\n  /**\n   * High-level coordinator: parse input, clone if URL, determine discovery vs direct mode.\n   * @param {string} input - URL or local path\n   * @param {Object} [options] - Options passed to cloneRepo\n   * @returns {Object} { parsed, rootDir, repoPath, sourceUrl, marketplace, mode: 'discovery'|'direct' }\n   */\n  async resolveSource(input, options = {}) {\n    const parsed = this.parseSource(input);\n    if (!parsed.isValid) throw new Error(parsed.error);\n\n    let rootDir;\n    let repoPath;\n    let sourceUrl;\n\n    if (parsed.type === 'local') {\n      rootDir = parsed.localPath;\n      repoPath = null;\n      sourceUrl = null;\n    } else {\n      repoPath = await this.cloneRepo(input, options);\n      sourceUrl = parsed.cloneUrl;\n      rootDir = parsed.subdir ? path.join(repoPath, parsed.subdir) : repoPath;\n\n      if (parsed.subdir && !(await fs.pathExists(rootDir))) {\n        throw new Error(`Subdirectory '${parsed.subdir}' not found in cloned repository`);\n      }\n    }","sourceCodeStart":313,"sourceCodeEnd":349,"githubUrl":"https://github.com/bmad-code-org/BMAD-METHOD/blob/b70486b9bdcb0a404d329e2a763b57964e7f1360/tools/installer/modules/custom-module-manager.js#L313-L349","documentation":"Thrown by resolveSource() when parseSource() identifies the input as a local path but it carries an @version suffix (e.g., './my-module@1.2.0'). Local paths cannot be versioned because they point at a fixed directory on disk — version resolution only applies to remote Git URLs.","triggerScenarios":"Calling resolveSource('./local/dir@v2') or resolveSource('~/modules/repo@main'). The parser strips the @suffix, detects the remainder as a local path (starts with /, ./, ../, ~, or is a Windows absolute path), and returns isValid:false.","commonSituations":"A user copies a URL install pattern (URL@version) to a local path; a script appends a version to a path programmatically without distinguishing URL vs. local sources.","solutions":["Remove the @version suffix from the local path — local directories are used as-is.","If you need versioning, install from a Git URL instead of a local path.","If the path genuinely contains an '@' in a directory name, rename the directory or use a Git remote."],"exampleFix":"// before\nawait mgr.resolveSource('./modules/repo@1.2.0');\n\n// after\nawait mgr.resolveSource('./modules/repo');","handlingStrategy":"validation","validationCode":"// Detect local path + version suffix before calling resolveSource\nfunction isLocalWithVersion(input) {\n  const localPrefixes = ['/', './', '../', '.\\\\', '..\\\\', '~'];\n  const isLocal = localPrefixes.some(p => input.startsWith(p)) || path.win32.isAbsolute(input);\n  if (!isLocal) return false;\n  const lastAt = input.lastIndexOf('@');\n  return lastAt > 0 && /^[\\w.\\-+/]+$/.test(input.slice(lastAt + 1));\n}\n\nif (isLocalWithVersion(input)) {\n  throw new Error('Remove the @version suffix from local paths');\n}","typeGuard":null,"tryCatchPattern":"try {\n  await mgr.resolveSource(input);\n} catch (e) {\n  if (e.message === 'Local paths do not support @version suffixes') {\n    const cleanInput = input.replace(/@[\\w.\\-+/]+$/, '');\n    console.log(`Retrying without version suffix: ${cleanInput}`);\n    await mgr.resolveSource(cleanInput);\n    return;\n  }\n  throw e;\n}","preventionTips":["Strip @version suffixes from local paths before passing to the installer.","Distinguish between URL and local path inputs in your code before formatting.","Educate users that version pinning only applies to remote Git URLs."],"tags":["input-validation","local-path","version-suffix"],"backgroundTag":null,"analyzedSha":"b70486b9bdcb0a404d329e2a763b57964e7f1360","analyzedAt":"2026-08-13T01:21:12.247Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}