{"record":{"id":"434a714ef020f186","repo":"bmad-code-org/BMAD-METHOD","slug":"version-is-not-available-when-nothing-is-installed","errorCode":null,"errorMessage":"version is not available when nothing is installed","messagePattern":"version is not available when nothing is installed","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"tools/installer/core/existing-install.js","lineNumber":25,"sourceCode":" * Immutable snapshot of an existing BMAD installation.\n * Pure query object — no filesystem operations after construction.\n */\nclass ExistingInstall {\n  #version;\n\n  constructor({ installed, version, hasCore, modules, ides }) {\n    this.installed = installed;\n    this.#version = version;\n    this.hasCore = hasCore;\n    this.modules = Object.freeze(modules.map((m) => Object.freeze({ ...m })));\n    this.moduleIds = Object.freeze(this.modules.map((m) => m.id));\n    this.ides = Object.freeze([...ides]);\n    Object.freeze(this);\n  }\n\n  get version() {\n    if (!this.installed) {\n      throw new Error('version is not available when nothing is installed');\n    }\n    return this.#version;\n  }\n\n  static empty() {\n    return new ExistingInstall({\n      installed: false,\n      version: null,\n      hasCore: false,\n      modules: [],\n      ides: [],\n    });\n  }\n\n  /**\n   * Scan a bmad directory and return an immutable snapshot of what's installed.\n   * @param {string} bmadDir - Path to bmad directory\n   * @returns {Promise<ExistingInstall>}","sourceCodeStart":7,"sourceCodeEnd":43,"githubUrl":"https://github.com/bmad-code-org/BMAD-METHOD/blob/b70486b9bdcb0a404d329e2a763b57964e7f1360/tools/installer/core/existing-install.js#L7-L43","documentation":"Thrown by the ExistingInstall.version getter when the installation snapshot reports installed=false. ExistingInstall.empty() (returned when no bmad dir or no core/modules/manifest are detected) always sets installed=false, so reading .version on it is undefined. The getter exists to fail fast rather than return null silently.","triggerScenarios":"Calling `existingInstall.version` on an instance produced by `ExistingInstall.empty()`, or on any ExistingInstall where `detect()` found no core directory, no modules, and no manifest.yaml. Happens in code paths that skip the `existingInstall.installed` guard before reading version.","commonSituations":"Running uninstall/status/update commands against a directory that never had BMAD installed, or where _bmad/ was deleted by hand. A caller that assumes `.version` is always populated after `detect()`.","solutions":["Check `existingInstall.installed` before reading `existingInstall.version` (mirror uninstall.js:65).","If you need a display string, use `existingInstall.installed ? existingInstall.version : 'unknown'`.","Ensure the directory passed to detect() actually contains a _bmad/ folder."],"exampleFix":"// before\nconst install = await ExistingInstall.detect(bmadDir);\nconsole.log(install.version);\n\n// after\nconst install = await ExistingInstall.detect(bmadDir);\nconsole.log(install.installed ? install.version : 'unknown');","handlingStrategy":"type-guard","validationCode":"const install = await ExistingInstall.detect(bmadDir);\nif (!install.installed) {\n  // handle no-install case without reading .version\n  return;\n}","typeGuard":"function hasVersion(install) {\n  return install instanceof ExistingInstall && install.installed === true;\n}","tryCatchPattern":null,"preventionTips":["Always branch on `existingInstall.installed` before reading `.version`.","Treat ExistingInstall.empty() as 'no install' and skip version-dependent logic.","Mirror the guard pattern used in commands/uninstall.js."],"tags":["state-guard","installer","api-misuse"],"backgroundTag":null,"analyzedSha":"b70486b9bdcb0a404d329e2a763b57964e7f1360","analyzedAt":"2026-08-13T01:21:12.247Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}