{"record":{"id":"bf05557c43f72575","repo":"bmad-code-org/BMAD-METHOD","slug":"path-must-be-a-string","errorCode":null,"errorMessage":"Path must be a string.","messagePattern":"Path must be a string\\.","errorType":"exception","errorClass":"TypeError","httpStatus":null,"severity":"error","filePath":"tools/installer/ui.js","lineNumber":1659,"sourceCode":"      // Stop at root\n      const parent = path.dirname(currentPath);\n      if (await fs.pathExists(parent)) {\n        return parent;\n      }\n      currentPath = parent;\n    }\n\n    return null; // No existing parent found (shouldn't happen in practice)\n  }\n\n  /**\n   * Expands the user-provided path: handles ~ and resolves to absolute.\n   * @param {string} inputPath - User input path.\n   * @returns {string} Absolute expanded path.\n   */\n  expandUserPath(inputPath) {\n    if (typeof inputPath !== 'string') {\n      throw new TypeError('Path must be a string.');\n    }\n\n    let expanded = inputPath.trim();\n\n    // Handle tilde expansion\n    if (expanded.startsWith('~')) {\n      if (expanded === '~') {\n        expanded = os.homedir();\n      } else if (expanded.startsWith('~' + path.sep)) {\n        const pathAfterHome = expanded.slice(2); // Remove ~/ or ~\\\n        expanded = path.join(os.homedir(), pathAfterHome);\n      } else {\n        const restOfPath = expanded.slice(1);\n        const separatorIndex = restOfPath.indexOf(path.sep);\n        const username = separatorIndex === -1 ? restOfPath : restOfPath.slice(0, separatorIndex);\n        if (username) {\n          throw new Error(`Path expansion for ~${username} is not supported. Please use an absolute path or ~${path.sep}`);\n        }","sourceCodeStart":1641,"sourceCodeEnd":1677,"githubUrl":"https://github.com/bmad-code-org/BMAD-METHOD/blob/b70486b9bdcb0a404d329e2a763b57964e7f1360/tools/installer/ui.js#L1641-L1677","documentation":"Thrown by expandUserPath (as TypeError) when inputPath is not a string. The method does inputPath.trim() and tilde expansion, both of which require a string; non-string input is rejected at the top before any path logic runs.","triggerScenarios":"Calling expandUserPath(undefined), expandUserPath(null), or passing a number/object. Indirectly when options.directory is truthy but not a string and is forwarded here.","commonSituations":"Programmatic calls passing undefined; a CLI option parsed in a way that yields a non-string; config loading returning undefined that isn't defaulted.","solutions":["Ensure the value passed is a string before calling expandUserPath.","Guard callers: if (typeof dir === 'string') expandUserPath(dir).","Default options.directory to undefined and skip the directory branch when unset, rather than forwarding undefined."],"exampleFix":"// before\n//   const dir = this.expandUserPath(options.directory); // options.directory may be undefined\n//\n// after\n//   if (typeof options.directory !== 'string') {\n//     throw new Error('options.directory must be a string');\n//   }\n//   const dir = this.expandUserPath(options.directory);","handlingStrategy":"type-guard","validationCode":"if (options.directory != null && typeof options.directory !== 'string') {\n  throw new TypeError('options.directory must be a string');\n}\nif (typeof options.directory === 'string') {\n  const dir = ui.expandUserPath(options.directory);\n}","typeGuard":"function isStringPath(p) { return typeof p === 'string'; }","tryCatchPattern":"try {\n  dir = ui.expandUserPath(options.directory);\n} catch (e) {\n  if (/Path must be a string/.test(e.message)) {\n    if (typeof options.directory !== 'string') options.directory = undefined;\n  } else { throw e; }\n}","preventionTips":["Type-check CLI options at the boundary before forwarding them to expandUserPath.","Default optional path options to undefined and branch on typeof === 'string'."],"tags":["installer","path-expansion","type-error"],"backgroundTag":null,"analyzedSha":"b70486b9bdcb0a404d329e2a763b57964e7f1360","analyzedAt":"2026-08-13T01:21:12.247Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}