///|
/// One workspace member: directory, module name, current version, and the
/// member modules it imports.
pub(all) struct Member {
  dir : String
  name : String
  version : String
  imports : Array[String]
}

///|
pub(all) suberror ManifestError {
  Missing(dir~ : String)
}

///|
pub impl Show for ManifestError with fn output(self, logger) {
  match self {
    Missing(dir~) => {
      logger.write_string("manifest is missing name or version: ")
      logger.write_string(dir)
    }
  }
}

///|
pub extend ManifestError with Show::{to_string, output}

///|
/// Directory names from the `members` list of moon.work.
pub fn parse_members(text : String) -> Array[String] {
  let dirs = []
  let mut inside = false
  for line in text.split("\n") {
    let trimmed = @fndash.trim_spaces(line)
    if !inside {
      if trimmed.has_prefix("members") && trimmed.contains("[") {
        inside = true
      }
      continue
    }
    if trimmed.has_prefix("]") || trimmed.has_prefix("#") {
      break
    }
    // A member line looks like `"bm2",` — keep the quoted part.
    let bytes = @utf8.encode(trimmed)
    let mut start = -1
    for i, byte in bytes {
      if byte.to_int() == 0x22 && start < 0 {
        start = i
      } else if byte.to_int() == 0x22 && start >= 0 {
        dirs.push(trimmed[start + 1:i].to_owned())
        break
      }
    }
  }
  dirs
}

///|
/// Parse one moon.mod text: module name, version and the module paths in
/// the import list (any @version suffix stripped). Raises Missing when
/// name or version cannot be found.
pub fn parse_mod(dir : String, text : String) -> Member raise ManifestError {
  let mut name = ""
  let mut version = ""
  let imports : Array[String] = []
  let mut in_imports = false
  for line in text.split("\n") {
    let trimmed = @fndash.trim_spaces(line)
    if trimmed.has_prefix("import") {
      in_imports = true
      continue
    }
    if in_imports {
      if trimmed.has_prefix("\"") {
        // An import line: strip the surrounding quotes and any @version
        // suffix. The line starts with the opening quote at index 0, so the
        // scan for the closing one starts at 1.
        let mut stop = trimmed.length()
        for i in 1..