///|
priv enum TsmbtDirection {
  Auto
  TsToMbt
  MbtToTs
}

///|
priv struct UnifiedTsmbtOptions {
  input : String?
  out : String?
  direction : TsmbtDirection
  module_spec : String?
  import_rewrite_path : String?
  diagnostics_path : String?
  strict : Bool
  facade : Bool
}

///|
/// Options that belong to the project-level `mbt2ts --pkg` entrypoint.
/// Unlike the lower-level unified input flow, this command owns `moon info`,
/// derives package metadata from the module manifest, and defaults to a
/// publish directory at `/npm`.
priv struct Mbt2tsPkgOptions {
  out : String?
  import_rewrite_path : String?
  strict : Bool
  facade : Bool
}

///|
/// An executable source package to publish through `package.json#bin`.
/// Commands stay out of the TypeScript library export graph, but their
/// compiled JavaScript remains available to npm's command shim.
priv struct Mbt2tsPkgCommand {
  name : String
  source_relative_dir : String
}

///|
fn parse_mbt2ts_pkg_options(
  args : Array[String],
  start : Int,
) -> (Mbt2tsPkgOptions?, String?) {
  let mut out : String? = None
  let mut import_rewrite_path : String? = None
  let mut strict = false
  let mut facade = true
  let mut idx = start
  while idx < args.length() {
    let arg = args[idx]
    match arg {
      "--help" | "-h" => return (None, Some("help"))
      "--out" | "-o" => {
        if idx + 1 >= args.length() {
          return (None, Some("missing value for \{arg}"))
        }
        out = Some(args[idx + 1])
        idx += 2
        continue
      }
      "--import-rewrites" | "--import-rewrite" => {
        if idx + 1 >= args.length() {
          return (None, Some("missing value for \{arg}"))
        }
        import_rewrite_path = Some(args[idx + 1])
        idx += 2
        continue
      }
      "--strict" => {
        strict = true
        idx += 1
        continue
      }
      "--no-strict" => {
        strict = false
        idx += 1
        continue
      }
      "--facade" => {
        facade = true
        idx += 1
        continue
      }
      "--no-facade" => {
        facade = false
        idx += 1
        continue
      }
      _ => ()
    }
    if arg.has_prefix("--out=") {
      out = Some(arg["--out=".length():arg.length()].to_owned())
    } else if arg.has_prefix("--import-rewrites=") {
      import_rewrite_path = Some(
        arg["--import-rewrites=".length():arg.length()].to_owned(),
      )
    } else if arg.has_prefix("--import-rewrite=") {
      import_rewrite_path = Some(
        arg["--import-rewrite=".length():arg.length()].to_owned(),
      )
    } else {
      return (None, Some("unknown option: \{arg}"))
    }
    idx += 1
  }
  (Some({ out, import_rewrite_path, strict, facade }), None)
}

///|
fn parse_tsmbt_direction(value : String) -> TsmbtDirection? {
  match value {
    "auto" => Some(TsmbtDirection::Auto)
    "ts-to-mbt" | "ts2mbt" | "ts" => Some(TsmbtDirection::TsToMbt)
    "mbt-to-ts" | "mbt2ts" | "mbt" => Some(TsmbtDirection::MbtToTs)
    _ => None
  }
}

///|
fn parse_unified_tsmbt_options(
  args : Array[String],
  start : Int,
) -> (UnifiedTsmbtOptions?, String?) {
  let mut input : String? = None
  let mut out : String? = None
  let mut direction = TsmbtDirection::Auto
  let mut module_spec : String? = None
  let mut import_rewrite_path : String? = None
  let mut diagnostics_path : String? = None
  let mut strict = false
  let mut facade = true
  let mut idx = start
  while idx < args.length() {
    let arg = args[idx]
    match arg {
      "--help" | "-h" => return (None, Some("help"))
      "--input" | "-i" => {
        if idx + 1 >= args.length() {
          return (None, Some("missing value for \{arg}"))
        }
        input = Some(args[idx + 1])
        idx += 2
        continue
      }
      "--out" | "-o" => {
        if idx + 1 >= args.length() {
          return (None, Some("missing value for \{arg}"))
        }
        out = Some(args[idx + 1])
        idx += 2
        continue
      }
      "--direction" => {
        if idx + 1 >= args.length() {
          return (None, Some("missing value for --direction"))
        }
        match parse_tsmbt_direction(args[idx + 1]) {
          Some(parsed) => direction = parsed
          None =>
            return (
              None,
              Some(
                "invalid --direction '\{args[idx + 1]}', expected auto, mbt-to-ts, or ts-to-mbt",
              ),
            )
        }
        idx += 2
        continue
      }
      "--module-spec" | "--runtime-module" => {
        if idx + 1 >= args.length() {
          return (None, Some("missing value for \{arg}"))
        }
        module_spec = Some(args[idx + 1])
        idx += 2
        continue
      }
      "--import-rewrites" | "--import-rewrite" => {
        if idx + 1 >= args.length() {
          return (None, Some("missing value for \{arg}"))
        }
        import_rewrite_path = Some(args[idx + 1])
        idx += 2
        continue
      }
      "--diagnostics" => {
        if idx + 1 >= args.length() {
          return (None, Some("missing value for --diagnostics"))
        }
        diagnostics_path = Some(args[idx + 1])
        idx += 2
        continue
      }
      "--strict" => {
        strict = true
        idx += 1
        continue
      }
      "--no-strict" => {
        strict = false
        idx += 1
        continue
      }
      "--facade" => {
        facade = true
        idx += 1
        continue
      }
      "--no-facade" => {
        facade = false
        idx += 1
        continue
      }
      _ => ()
    }
    if arg.has_prefix("--input=") {
      input = Some(arg["--input=".length():arg.length()].to_owned())
    } else if arg.has_prefix("--out=") {
      out = Some(arg["--out=".length():arg.length()].to_owned())
    } else if arg.has_prefix("--direction=") {
      let value = arg["--direction=".length():arg.length()].to_owned()
      match parse_tsmbt_direction(value) {
        Some(parsed) => direction = parsed
        None =>
          return (
            None,
            Some(
              "invalid --direction '\{value}', expected auto, mbt-to-ts, or ts-to-mbt",
            ),
          )
      }
    } else if arg.has_prefix("--module-spec=") {
      module_spec = Some(arg["--module-spec=".length():arg.length()].to_owned())
    } else if arg.has_prefix("--runtime-module=") {
      module_spec = Some(
        arg["--runtime-module=".length():arg.length()].to_owned(),
      )
    } else if arg.has_prefix("--import-rewrites=") {
      import_rewrite_path = Some(
        arg["--import-rewrites=".length():arg.length()].to_owned(),
      )
    } else if arg.has_prefix("--import-rewrite=") {
      import_rewrite_path = Some(
        arg["--import-rewrite=".length():arg.length()].to_owned(),
      )
    } else if arg.has_prefix("--diagnostics=") {
      diagnostics_path = Some(
        arg["--diagnostics=".length():arg.length()].to_owned(),
      )
    } else if !arg.has_prefix("-") && input is None {
      input = Some(arg)
    } else if !arg.has_prefix("-") && out is None {
      out = Some(arg)
    } else {
      return (None, Some("unknown option: \{arg}"))
    }
    idx += 1
  }
  (
    Some({
      input,
      out,
      direction,
      module_spec,
      import_rewrite_path,
      diagnostics_path,
      strict,
      facade,
    }),
    None,
  )
}

///|
async fn main_file_exists(path : String) -> Bool {
  let kind = @fs.kind(path, follow_symlink=false) catch {
    _ => @fs.FileKind::Unknown
  }
  kind is @fs.FileKind::Regular
}

///|
fn unified_tsmbt_skip_scan_dir(name : String) -> Bool {
  name == ".git" ||
  name == ".mooncakes" ||
  name == "_build" ||
  name == "target" ||
  name == "node_modules" ||
  name == "test262"
}

///|
async fn collect_pkg_generated_mbti_files(
  path : String,
  out : Array[String],
) -> Unit {
  let kind = @fs.kind(path, follow_symlink=false) catch {
    _ => @fs.FileKind::Unknown
  }
  match kind {
    @fs.FileKind::Directory => {
      let entries = @fs.readdir(
        path,
        include_hidden=false,
        include_special=false,
        sort=true,
      ) catch {
        _ => []
      }
      for name in entries {
        if unified_tsmbt_skip_scan_dir(name) {
          continue
        }
        collect_pkg_generated_mbti_files(main_join_path(path, name), out)
      }
    }
    @fs.FileKind::Regular =>
      if path.has_suffix("pkg.generated.mbti") {
        out.push(path)
      }
    _ => ()
  }
}

///|
fn parse_mbti_package_name_from_source(source : String) -> String? {
  for line_view in source.split("\n") {
    let line = line_view.trim().to_owned()
    if !line.has_prefix("package \"") {
      continue
    }
    let rest = line["package \"".length():line.length()].to_owned()
    match rest.find("\"") {
      Some(end_idx) => return Some(rest[:end_idx].to_owned())
      None => ()
    }
  }
  None
}

///|
async fn resolve_unified_mbt_input_path(input : String) -> String? {
  let roots : Array[String] = ["."]
  match infer_ghq_github_root_from_cwd() {
    Some(root) => roots.push(root)
    None => ()
  }
  resolve_unified_mbt_input_path_from_roots(input, roots)
}

///|
fn ghq_candidate_repo_paths(
  github_root : String,
  input : String,
) -> Array[String] {
  let paths = [main_join_path(github_root, input)]
  if !input.has_suffix(".mbt") {
    paths.push(main_join_path(github_root, input + ".mbt"))
  }
  paths
}

///|
async fn ghq_module_name_candidate_repo_paths(
  github_root : String,
  input : String,
) -> Array[String] {
  let paths : Array[String] = []
  let owner = match input.find("/") {
    Some(sep_idx) => input[:sep_idx].to_owned()
    None => return paths
  }
  let owner_root = main_join_path(github_root, owner)
  if !@fs.exists(owner_root) {
    return paths
  }
  let entries = @fs.readdir(
    owner_root,
    include_hidden=false,
    include_special=false,
    sort=true,
  ) catch {
    _ => return paths
  }
  for name in entries {
    if unified_tsmbt_skip_scan_dir(name) {
      continue
    }
    let repo_path = main_join_path(owner_root, name)
    let kind = @fs.kind(repo_path, follow_symlink=false) catch {
      _ => @fs.FileKind::Unknown
    }
    if !(kind is @fs.FileKind::Directory) {
      continue
    }
    let moon_mod_source = match read_moon_mod_source(repo_path) {
      Some(source) => source
      None => continue
    }
    match parse_moon_mod_string_field(moon_mod_source, "name") {
      Some(module_name) if module_name == input => paths.push(repo_path)
      _ => ()
    }
  }
  paths
}

///|
async fn infer_ghq_github_root_from_cwd() -> String? {
  let cwd = @fs.realpath(".") catch { _ => "." }
  match cwd.find("/ghq/github.com/") {
    Some(idx) => Some(cwd[:idx + "/ghq/github.com".length()].to_owned())
    None => None
  }
}

///|
async fn resolve_unified_mbt_input_path_from_roots(
  input : String,
  roots : Array[String],
) -> String? {
  if main_file_exists(input) {
    return Some(input)
  }
  let direct_pkg_path = main_join_path(input, "pkg.generated.mbti")
  if main_file_exists(direct_pkg_path) {
    return Some(direct_pkg_path)
  }
  let scan_order : Array[String] = []
  for root in roots {
    if root.has_suffix("/github.com") && !scan_order.contains(root) {
      scan_order.push(root)
    }
  }
  for root in roots {
    if !root.has_suffix("/github.com") && !scan_order.contains(root) {
      scan_order.push(root)
    }
  }
  for root in scan_order {
    let scan_roots = if root.has_suffix("/github.com") && input.contains("/") {
      let paths = ghq_candidate_repo_paths(root, input)
      for path in ghq_module_name_candidate_repo_paths(root, input) {
        if !paths.contains(path) {
          paths.push(path)
        }
      }
      paths
    } else {
      [root]
    }
    for scan_root in scan_roots {
      if !@fs.exists(scan_root) {
        continue
      }
      let candidates : Array[String] = []
      collect_pkg_generated_mbti_files(scan_root, candidates)
      for path in candidates {
        let source = @fs.read_file(path).text() catch { _ => continue }
        match parse_mbti_package_name_from_source(source) {
          Some(package_name) if package_name == input => return Some(path)
          _ => ()
        }
      }
    }
  }
  None
}

///|
fn is_ts_input_path_like(input : String) -> Bool {
  input.has_suffix(".d.ts") ||
  input.has_suffix(".d.mts") ||
  input.has_suffix(".d.cts") ||
  input.has_suffix(".ts") ||
  input.has_suffix(".tsx") ||
  input.has_suffix(".mts") ||
  input.has_suffix(".cts")
}

///|
async fn resolve_unified_ts_input_path(input : String) -> String? {
  if main_file_exists(input) {
    return Some(input)
  }
  @parser.resolve_type_module_specifier("__tsmbt_entry__.ts", input)
}

///|
async fn infer_unified_tsmbt_direction(input : String) -> TsmbtDirection? {
  if is_ts_input_path_like(input) {
    return Some(TsmbtDirection::TsToMbt)
  }
  if input.has_suffix(".mbti") {
    return Some(TsmbtDirection::MbtToTs)
  }
  match resolve_unified_mbt_input_path(input) {
    Some(_) => return Some(TsmbtDirection::MbtToTs)
    None => ()
  }
  match resolve_unified_ts_input_path(input) {
    Some(_) => Some(TsmbtDirection::TsToMbt)
    None => None
  }
}

///|
fn unified_ts_scaffold_diagnostics_path(
  output_dir : String,
  diagnostics_path : String?,
) -> String {
  match diagnostics_path {
    Some(path) => path
    None => main_join_path(output_dir, "SCAFFOLD_DIAGNOSTICS.md")
  }
}

///|
fn unified_ts_fallback_policy_for_package(
  package_spec : String,
) -> (String, String) {
  match package_spec {
    "clsx"
    | "node:path"
    | "node:crypto"
    | "node:os"
    | "node:url"
    | "node:querystring"
    | "node:buffer" =>
      (
        "zero-target", "Keep public JSValue surface at zero; any fallback is a regression.",
      )
    "hono" =>
      (
        "naturalize-target", "Reduce generic context/router fallbacks while keeping route handlers and response helpers natural.",
      )
    "react-router" =>
      (
        "naturalize-target", "Reduce route/path utility overload and option-object fallbacks; keep typed navigation helpers usable from MoonBit.",
      )
    "jose" =>
      (
        "naturalize-target", "Reduce builder option and compact JWS/JWT overload fallbacks around the smoke-tested APIs.",
      )
    "glob" =>
      (
        "naturalize-target", "Reduce pattern/options namespace fallbacks for common sync glob calls.",
      )
    "node:assert" | "node:util" =>
      (
        "naturalize-target", "Shrink remaining Node built-in overload/unknown fallbacks toward zero for the common API surface.",
      )
    "date-fns" | "magic-string" | "source-map" | "node:sqlite" | "node:fs" =>
      (
        "naturalize-target", "Keep reducing fallback around finite option bags, tuple results, and class/value helper APIs.",
      )
    "zod" | "valibot" =>
      (
        "budgeted-fallback", "Schema/parser generics are intentionally smoke-tested and budgeted, not treated as naturally typed MoonBit APIs yet.",
      )
    "preact" =>
      (
        "budgeted-fallback", "JSX/component/children generics are intentionally budgeted until a dedicated JSX/component binding layer exists.",
      )
    "playwright" =>
      (
        "budgeted-fallback", "Large event/callback-heavy API is smoke-tested; only selected launch/device/options surfaces are naturalization targets.",
      )
    "chalk"
    | "dotenv"
    | "ignore"
    | "colorette"
    | "immer"
    | "execa"
    | "vitest/runtime"
    | "express" =>
      (
        "low-fallback-maintain", "Current fallback is small and explicitly budgeted; naturalize only when a real smoke use case needs it.",
      )
    _ =>
      (
        "unclassified", "Add an explicit fallback policy before accepting this package into the real-world corpus.",
      )
  }
}

///|
fn render_unified_ts_fallback_policy_md(module_spec : String) -> String {
  let (class_name, policy) = unified_ts_fallback_policy_for_package(module_spec)
  [
    "## Fallback Policy",
    "",
    "This classification mirrors the real-world bridge quality policy. It is informational in non-strict mode; strict mode still rejects generated `JSValue` fallbacks.",
    "",
    "| package | class | policy |",
    "| --- | --- | --- |",
    "| `\{module_spec}` | \{class_name} | \{policy} |",
  ].join("\n")
}

///|
fn render_unified_ts_scaffold_diagnostics_md(
  module_spec : String,
  unsupported_exports : Array[String],
  jsvalue_fallbacks : Array[String],
) -> String {
  let mut base = render_moonbit_scaffold_diagnostics_md(unsupported_exports)
  // Avoid the contradictory "No unsupported exports were detected." banner
  // when the JSValue fallback list below it is non-empty: those JSValue
  // occurrences are themselves widened TypeScript boundary surfaces and
  // count as unsupported in any practical sense.
  if unsupported_exports.length() == 0 && jsvalue_fallbacks.length() > 0 {
    base = base.replace(
      old="No unsupported exports were detected.",
      new="No structural unsupported exports were detected; \{jsvalue_fallbacks.length()} `JSValue` boundary fallbacks are listed below.",
    )
  }
  let fallback_policy = render_unified_ts_fallback_policy_md(module_spec)
  if jsvalue_fallbacks.length() == 0 {
    return [base, "", fallback_policy].join("\n")
  }
  let lines = [
    base, "", fallback_policy, "", "## JSValue Fallbacks", "", "Strict mode treats these generated `JSValue` occurrences as unbudgeted TypeScript boundary fallbacks.",
    "",
  ]
  for item in jsvalue_fallbacks {
    lines.push("- " + item)
  }
  lines.join("\n")
}

///|
async fn write_unified_ts_scaffold_diagnostics(
  output_dir : String,
  diagnostics_path : String?,
  module_spec : String,
  unsupported_exports : Array[String],
  jsvalue_fallbacks : Array[String],
) -> Bool {
  let path = unified_ts_scaffold_diagnostics_path(output_dir, diagnostics_path)
  if !write_text_file(
      path,
      render_unified_ts_scaffold_diagnostics_md(
        module_spec, unsupported_exports, jsvalue_fallbacks,
      ),
    ) {
    return false
  }
  println("Wrote scaffold diagnostics to \{path}")
  true
}

///|
fn collect_unified_jsvalue_fallbacks_from_source(
  relative_path : String,
  source : String,
  out : Array[String],
) -> Unit {
  let mut line_no = 1
  for line_view in source.split("\n") {
    let line = line_view.trim().to_owned()
    if unified_jsvalue_line_is_fallback(line) {
      out.push("\{relative_path}:\{line_no}: \{line}")
    }
    line_no += 1
  }
}

///|
fn unified_jsvalue_line_is_fallback(line : String) -> Bool {
  if !line.contains("JSValue") {
    return false
  }
  match line {
    "/// Complex or unsupported TypeScript types are widened to JSValue."
    | "declare pub type JSValue"
    | "pub type JSValue"
    | "pub type JSValue = @js.Any" => false
    _ => true
  }
}

///|
async fn collect_unified_moonbit_scaffold_jsvalue_fallbacks(
  output_dir : String,
) -> Array[String] {
  let fallbacks : Array[String] = []
  for relative_path in ["bridge.mbti", "bridge.mbt"] {
    let path = main_join_path(output_dir, relative_path)
    let source = @fs.read_file(path).text() catch { _ => continue }
    collect_unified_jsvalue_fallbacks_from_source(
      relative_path, source, fallbacks,
    )
  }
  fallbacks.sort()
  fallbacks
}

///|
fn unified_autolink_diagnostics_has_omissions(source : String) -> Bool {
  source.contains("\n## ")
}

///|
async fn write_requested_unified_autolink_diagnostics(
  output_dir : String,
  diagnostics_path : String?,
) -> Bool {
  let path = match diagnostics_path {
    Some(path) => path
    None => return true
  }
  let autolink_path = main_join_path(output_dir, "AUTOLINK_DIAGNOSTICS.md")
  let diagnostics_md = @fs.read_file(autolink_path).text() catch {
    e => {
      println("Read error: \{e}")
      return false
    }
  }
  if !write_text_file(path, diagnostics_md) {
    return false
  }
  println("Wrote autolink diagnostics to \{path}")
  true
}

///|
async fn emit_unified_tsmbt_scaffold(
  input : String,
  out : String,
  direction~ : TsmbtDirection,
  module_spec~ : String?,
  import_rewrite_path~ : String?,
  diagnostics_path? : String? = None,
  strict? : Bool = false,
  facade~ : Bool,
) -> Bool {
  let resolved_direction = match direction {
    TsmbtDirection::Auto =>
      match infer_unified_tsmbt_direction(input) {
        Some(direction) => direction
        None => {
          println(
            "Error: could not infer direction for '\{input}'. Use --direction mbt-to-ts or --direction ts-to-mbt.",
          )
          return false
        }
      }
    _ => direction
  }
  match resolved_direction {
    TsmbtDirection::MbtToTs => {
      let mbti_path = match resolve_unified_mbt_input_path(input) {
        Some(path) => path
        None => {
          println(
            "Error: could not resolve MoonBit package or pkg.generated.mbti '\{input}'. Run moon info first or pass a pkg.generated.mbti path.",
          )
          return false
        }
      }
      let ok = if facade {
        emit_typescript_facade_scaffold_from_mbti(
          mbti_path, out, import_rewrite_path,
        )
      } else {
        emit_typescript_scaffold_from_mbti(mbti_path, out, import_rewrite_path)
      }
      if !ok {
        return false
      }
      let diagnostics_md = @fs.read_file(
        main_join_path(out, "AUTOLINK_DIAGNOSTICS.md"),
      ).text() catch {
        e => {
          println("Read error: \{e}")
          return false
        }
      }
      if !write_requested_unified_autolink_diagnostics(out, diagnostics_path) {
        return false
      }
      if strict && unified_autolink_diagnostics_has_omissions(diagnostics_md) {
        println(
          "Strict mode rejected MoonBit scaffold: omitted autolink members detected.",
        )
        return false
      }
      true
    }
    TsmbtDirection::TsToMbt => {
      let entry_path = match resolve_unified_ts_input_path(input) {
        Some(path) => path
        None => {
          println(
            "Error: could not resolve TypeScript entrypoint '\{input}'. Pass a .d.ts/.ts path or an installed package specifier.",
          )
          return false
        }
      }
      let runtime_module_spec = match module_spec {
        Some(spec) => spec
        None => input
      }
      let unsupported_exports = collect_moonbit_ts_scaffold_unsupported_exports(
        entry_path,
      ) catch {
        @bridge.ModuleGraphError::ReadError(msg)
        | @bridge.ModuleGraphError::ParseError(msg)
        | @bridge.ModuleGraphError::ResolveError(msg) => {
          println("Emit error: \{msg}")
          return false
        }
      }
      if strict && unsupported_exports.length() > 0 {
        if !write_unified_ts_scaffold_diagnostics(
            out,
            diagnostics_path,
            runtime_module_spec,
            unsupported_exports,
            [],
          ) {
          return false
        }
        println(
          "Strict mode rejected TypeScript scaffold: unsupported exports detected.",
        )
        return false
      }
      if !emit_moonbit_scaffold_from_ts(
          entry_path,
          runtime_module_spec,
          out,
          write_diagnostics=false,
        ) {
        return false
      }
      let jsvalue_fallbacks = collect_unified_moonbit_scaffold_jsvalue_fallbacks(
        out,
      )
      if !write_unified_ts_scaffold_diagnostics(
          out, diagnostics_path, runtime_module_spec, unsupported_exports, jsvalue_fallbacks,
        ) {
        return false
      }
      if strict && jsvalue_fallbacks.length() > 0 {
        println(
          "Strict mode rejected TypeScript scaffold: JSValue fallbacks detected.",
        )
        return false
      }
      true
    }
    TsmbtDirection::Auto => true
  }
}

///|
fn mbt2ts_pkg_path_is_absolute(path : String) -> Bool {
  path.has_prefix("/")
}

///|
fn mbt2ts_pkg_source_root(
  project_root : String,
  moon_mod_source : String,
) -> String {
  match parse_moon_mod_string_field(moon_mod_source, "source") {
    Some(source) if source != "" && source != "." =>
      main_join_path(project_root, source)
    _ => project_root
  }
}

///|
/// Development commands are executable implementation details, not library
/// subpath exports. Every other source directory with a `moon.pkg` becomes a
/// publishable npm submodule.
fn mbt2ts_pkg_skip_submodule_dir(name : String) -> Bool {
  unified_tsmbt_skip_scan_dir(name) ||
  name == "cmd" ||
  name.has_prefix("__tsmbt_")
}

///|
fn mbt2ts_pkg_skip_command_scan_dir(name : String) -> Bool {
  unified_tsmbt_skip_scan_dir(name) || name.has_prefix("__tsmbt_")
}

///|
fn mbt2ts_pkg_line_defines_main(line : String, prefix : String) -> Bool {
  if !line.has_prefix(prefix) {
    return false
  }
  if line.length() == prefix.length() {
    return true
  }
  let next = line[prefix.length()].unsafe_to_char()
  next == ' ' || next == '(' || next == '{'
}

///|
fn mbt2ts_pkg_source_defines_main(source : String) -> Bool {
  for line_view in source.split("\n") {
    let line = line_view.trim().to_owned()
    if mbt2ts_pkg_line_defines_main(line, "fn main") ||
      mbt2ts_pkg_line_defines_main(line, "async fn main") ||
      mbt2ts_pkg_line_defines_main(line, "pub fn main") ||
      mbt2ts_pkg_line_defines_main(line, "pub async fn main") {
      return true
    }
  }
  false
}

///|
fn mbt2ts_pkg_last_path_segment(path : String) -> String {
  match path.rev_find("/") {
    Some(idx) => path[idx + 1:path.length()].to_owned()
    None => path
  }
}

///|
fn mbt2ts_pkg_root_command_name(root_package_name : String) -> String {
  mbt2ts_pkg_last_path_segment(root_package_name)
}

///|
fn mbt2ts_pkg_command_name(
  source_relative_dir : String,
  root_package_name : String,
) -> String {
  if source_relative_dir == "" {
    mbt2ts_pkg_root_command_name(root_package_name)
  } else {
    mbt2ts_pkg_last_path_segment(source_relative_dir)
  }
}

///|
fn mbt2ts_pkg_quoted_strings(source : String) -> Array[String] {
  let values : Array[String] = []
  let mut cursor = 0
  while cursor < source.length() {
    let remaining = source[cursor:].to_owned()
    match remaining.find("\"") {
      None => return values
      Some(open_relative) => {
        let value_start = cursor + open_relative + 1
        let after_open = source[value_start:].to_owned()
        match after_open.find("\"") {
          None => return values
          Some(close_relative) => {
            values.push(
              source[value_start:value_start + close_relative].to_owned(),
            )
            cursor = value_start + close_relative + 1
          }
        }
      }
    }
  }
  values
}

///|
fn mbt2ts_pkg_normalize_excluded_source_dir(
  path : String,
  source_root_rel : String,
) -> String? {
  let normalized = path.trim().to_owned()
  if normalized == "" {
    return None
  }
  let source_root = source_root_rel.trim().to_owned()
  if source_root == "" || source_root == "." {
    return Some(normalized)
  }
  if normalized == source_root {
    return Some("")
  }
  let source_prefix = source_root + "/"
  if normalized.has_prefix(source_prefix) {
    return Some(normalized[source_prefix.length():].to_owned())
  }
  // MoonBit accepts an exclusion relative to the source root as well as a
  // module-root-relative one. Keeping the value lets either form match.
  Some(normalized)
}

///|
fn mbt2ts_pkg_manifest_excluded_command_dirs(
  moon_mod_source : String,
  source_root_rel : String,
) -> Array[String] {
  let dirs : Array[String] = []
  let mut cursor = 0
  while cursor < moon_mod_source.length() {
    let remaining = moon_mod_source[cursor:].to_owned()
    match remaining.find("exclude") {
      None => return dirs
      Some(exclude_relative) => {
        let after_key = cursor + exclude_relative + "exclude".length()
        let after_key_source = moon_mod_source[after_key:].to_owned()
        match after_key_source.find("[") {
          None => cursor = after_key
          Some(open_relative) => {
            let values_start = after_key + open_relative + 1
            let values_tail = moon_mod_source[values_start:].to_owned()
            match values_tail.find("]") {
              None => return dirs
              Some(close_relative) => {
                let values_source = moon_mod_source[values_start:values_start +
                close_relative].to_owned()
                for path in mbt2ts_pkg_quoted_strings(values_source) {
                  match
                    mbt2ts_pkg_normalize_excluded_source_dir(
                      path, source_root_rel,
                    ) {
                    Some(dir) if !dirs.contains(dir) => dirs.push(dir)
                    _ => ()
                  }
                }
                cursor = values_start + close_relative + 1
              }
            }
          }
        }
      }
    }
  }
  dirs
}

///|
fn mbt2ts_pkg_command_dir_is_excluded(
  source_relative_dir : String,
  excluded_dirs : Array[String],
) -> Bool {
  excluded_dirs.any(fn(excluded) {
    source_relative_dir == excluded ||
    source_relative_dir.has_prefix(excluded + "/")
  })
}

///|
async fn mbt2ts_pkg_collect_commands_from_dir(
  source_root : String,
  source_relative_dir : String,
  root_package_name : String,
  excluded_dirs : Array[String],
  commands : Array[Mbt2tsPkgCommand],
) -> Bool {
  if mbt2ts_pkg_command_dir_is_excluded(source_relative_dir, excluded_dirs) {
    return true
  }
  let dir = if source_relative_dir == "" {
    source_root
  } else {
    main_join_path(source_root, source_relative_dir)
  }
  let moon_pkg_path = main_join_path(dir, "moon.pkg")
  let main_path = main_join_path(dir, "main.mbt")
  if main_file_exists(moon_pkg_path) && main_file_exists(main_path) {
    let main_source = @fs.read_file(main_path).text() catch {
      e => {
        println("Error: --pkg could not read command source \{main_path}: \{e}")
        return false
      }
    }
    if mbt2ts_pkg_source_defines_main(main_source) {
      let name = mbt2ts_pkg_command_name(source_relative_dir, root_package_name)
      if name == "" || commands.any(fn(command) { command.name == name }) {
        println("Error: --pkg found duplicate or empty command name \{name}")
        return false
      }
      commands.push({ name, source_relative_dir })
    }
  }
  let entries = @fs.readdir(
    dir,
    include_hidden=false,
    include_special=false,
    sort=true,
  ) catch {
    e => {
      println("Error: --pkg could not read source directory \{dir}: \{e}")
      return false
    }
  }
  for name in entries {
    if mbt2ts_pkg_skip_command_scan_dir(name) {
      continue
    }
    let child_dir = main_join_path(dir, name)
    let kind = @fs.kind(child_dir, follow_symlink=false) catch {
      _ => @fs.FileKind::Unknown
    }
    if !(kind is @fs.FileKind::Directory) {
      continue
    }
    let child_relative_dir = if source_relative_dir == "" {
      name
    } else {
      source_relative_dir + "/" + name
    }
    if !mbt2ts_pkg_collect_commands_from_dir(
        source_root, child_relative_dir, root_package_name, excluded_dirs, commands,
      ) {
      return false
    }
  }
  true
}

///|
async fn mbt2ts_pkg_collect_commands(
  source_root : String,
  root_package_name : String,
  source_root_rel : String,
  moon_mod_source : String,
) -> Array[Mbt2tsPkgCommand]? {
  let commands : Array[Mbt2tsPkgCommand] = []
  let excluded_dirs = mbt2ts_pkg_manifest_excluded_command_dirs(
    moon_mod_source, source_root_rel,
  )
  if !mbt2ts_pkg_collect_commands_from_dir(
      source_root, "", root_package_name, excluded_dirs, commands,
    ) {
    return None
  }
  Some(commands)
}

///|
/// Collect generated interfaces for every library package below the source
/// root. `moon info` runs before this scan, so a package without its generated
/// interface is an actionable generation failure rather than a silent omission.
async fn mbt2ts_pkg_collect_submodule_mbti_paths(
  dir : String,
  paths : Array[String],
) -> Bool {
  let entries = @fs.readdir(
    dir,
    include_hidden=false,
    include_special=false,
    sort=true,
  ) catch {
    e => {
      println("Error: --pkg could not read source directory \{dir}: \{e}")
      return false
    }
  }
  for name in entries {
    if mbt2ts_pkg_skip_submodule_dir(name) {
      continue
    }
    let path = main_join_path(dir, name)
    let kind = @fs.kind(path, follow_symlink=false) catch {
      _ => @fs.FileKind::Unknown
    }
    if !(kind is @fs.FileKind::Directory) {
      continue
    }
    let moon_pkg_path = main_join_path(path, "moon.pkg")
    if main_file_exists(moon_pkg_path) {
      let mbti_path = main_join_path(path, "pkg.generated.mbti")
      if !main_file_exists(mbti_path) {
        println(
          "Error: moon info did not generate submodule interface \{mbti_path}",
        )
        return false
      }
      paths.push(mbti_path)
    }
    if !mbt2ts_pkg_collect_submodule_mbti_paths(path, paths) {
      return false
    }
  }
  true
}

///|
/// Select the local source submodules that belong to the root MoonBit package.
/// The generated umbrella MBTI below makes otherwise-unreferenced packages
/// visible to the existing recursive TypeScript scaffold emitter.
async fn mbt2ts_pkg_collect_submodule_names(
  source_root : String,
  root_package_name : String,
) -> Array[String]? {
  let paths : Array[String] = []
  if !mbt2ts_pkg_collect_submodule_mbti_paths(source_root, paths) {
    return None
  }
  let names : Array[String] = []
  let prefix = root_package_name + "/"
  for path in paths {
    let source = @fs.read_file(path).text() catch {
      e => {
        println("Error: --pkg could not read submodule interface \{path}: \{e}")
        return None
      }
    }
    let name = match parse_mbti_package_name_from_source(source) {
      Some(name) => name
      None => {
        println(
          "Error: --pkg submodule interface has no package declaration: \{path}",
        )
        return None
      }
    }
    if !name.has_prefix(prefix) {
      println(
        "Error: --pkg submodule \{name} is outside root package \{root_package_name}",
      )
      return None
    }
    if !names.contains(name) {
      names.push(name)
    }
  }
  names.sort()
  Some(names)
}

///|
/// Preserve the real root declarations while adding synthetic local imports.
/// The bridge emitter already knows how to turn a recursively loaded MBTI
/// graph into npm subpath exports; this only supplies the complete graph.
fn mbt2ts_pkg_render_umbrella_mbti(
  root_source : String,
  submodule_names : Array[String],
) -> String {
  let lines : Array[String] = []
  let mut inserted_imports = false
  for line_view in root_source.split("\n") {
    let line = line_view.to_owned()
    lines.push(line)
    if !inserted_imports && line.trim().has_prefix("package \"") {
      lines.push("")
      lines.push("import {")
      for idx in 0.. String {
  match requested {
    Some(path) if mbt2ts_pkg_path_is_absolute(path) => path
    Some(path) => main_join_path(project_root, path)
    None => main_join_path(project_root, "npm")
  }
}

///|
fn mbt2ts_pkg_command_build_target(
  source_root_rel : String,
  source_relative_dir : String,
) -> String {
  let root = if source_root_rel == "." { "" } else { source_root_rel }
  if root == "" {
    source_relative_dir
  } else if source_relative_dir == "" {
    root
  } else {
    root + "/" + source_relative_dir
  }
}

///|
fn mbt2ts_pkg_command_build_output_dir(
  module_root : String,
  command : Mbt2tsPkgCommand,
) -> String {
  let build_root = main_join_path(
    main_join_path(main_join_path(module_root, "_build"), "js"),
    "debug/build",
  )
  if command.source_relative_dir == "" {
    build_root
  } else {
    main_join_path(build_root, command.source_relative_dir)
  }
}

///|
async fn mbt2ts_pkg_copy_command_bin(
  built_js_path : String,
  command : Mbt2tsPkgCommand,
  output_dir : String,
) -> Bool {
  let source = @fs.read_file(built_js_path).text() catch {
    e => {
      println(
        "Error: --pkg could not read command build output \{built_js_path}: \{e}",
      )
      return false
    }
  }
  let command_js_name = command.name + ".js"
  let rendered = rewrite_moonbit_js_runtime_text(
    source,
    mbt2ts_pkg_last_path_segment(built_js_path) + ".map",
    command_js_name + ".map",
  )
  let bin_path = main_join_path(
    main_join_path(output_dir, "bin"),
    command_js_name,
  )
  let _ = ensure_dir_tree(main_dirname(bin_path)) catch {
    e => {
      println("Write error: could not create command bin directory: \{e}")
      return false
    }
  }
  let script = if rendered.has_prefix("#!") {
    rendered
  } else {
    "#!/usr/bin/env node\n" + rendered
  }
  let _ = @fs.write_file(bin_path, string_to_bytes(script), create=0o755) catch {
    e => {
      println("Write error: could not write command bin \{bin_path}: \{e}")
      return false
    }
  }
  let map_path = built_js_path + ".map"
  if @fs.exists(map_path) &&
    !copy_binary_file(
      map_path,
      main_join_path(
        main_join_path(output_dir, "bin"),
        command_js_name + ".map",
      ),
    ) {
    return false
  }
  true
}

///|
async fn emit_mbt2ts_pkg_command_bins(
  module_root : String,
  source_root_rel : String,
  output_dir : String,
  commands : Array[Mbt2tsPkgCommand],
) -> Bool {
  for command in commands {
    let target = mbt2ts_pkg_command_build_target(
      source_root_rel,
      command.source_relative_dir,
    )
    let args = ["-C", module_root, "build", "--target", "js"]
    if target != "" {
      args.push(target)
    }
    let (exit_code, output) = @process.collect_output_merged(
      "moon",
      args,
      cwd=".",
    )
    if exit_code != 0 {
      println("Error: --pkg could not build command \{command.name}")
      println(output.text())
      return false
    }
    let built_js_path = main_join_path(
      mbt2ts_pkg_command_build_output_dir(module_root, command),
      command.name + ".js",
    )
    if !mbt2ts_pkg_copy_command_bin(built_js_path, command, output_dir) {
      return false
    }
  }
  true
}

///|
fn mbt2ts_pkg_escape_json_string(source : String) -> String {
  let mut escaped = ""
  for c in source {
    if c == '\\' {
      escaped += "\\\\"
    } else if c == '"' {
      escaped += "\\\""
    } else if c == '\n' {
      escaped += "\\n"
    } else if c == '\r' {
      escaped += "\\r"
    } else if c == '\t' {
      escaped += "\\t"
    } else {
      escaped += c.to_string()
    }
  }
  escaped
}

///|
fn mbt2ts_pkg_publish_path_is_safe(path : String) -> Bool {
  path != "" &&
  !path.has_prefix("/") &&
  !path.contains("\\") &&
  !path.contains("..")
}

///|
fn mbt2ts_pkg_add_publish_path(paths : Array[String], path : String) -> Unit {
  if mbt2ts_pkg_publish_path_is_safe(path) && !paths.contains(path) {
    paths.push(path)
  }
}

///|
fn mbt2ts_pkg_collect_publish_paths(
  source : String,
  commands : Array[Mbt2tsPkgCommand],
) -> Array[String] {
  let paths : Array[String] = []
  for line_view in source.split("\n") {
    let line = line_view.to_owned()
    for marker in ["\"types\": \"./", "\"import\": \"./"] {
      match line.find(marker) {
        Some(index) => {
          let start = index + marker.length()
          let remainder = line[start:line.length()].to_owned()
          match remainder.find("\"") {
            Some(end) => {
              let path = remainder[:end].to_owned()
              mbt2ts_pkg_add_publish_path(paths, path)
              if path.has_suffix(".js") {
                mbt2ts_pkg_add_publish_path(paths, path + ".map")
              }
            }
            None => ()
          }
        }
        None => ()
      }
    }
  }
  for command in commands {
    let path = "bin/" + command.name + ".js"
    mbt2ts_pkg_add_publish_path(paths, path)
    mbt2ts_pkg_add_publish_path(paths, path + ".map")
  }
  paths.sort()
  paths
}

///|
fn mbt2ts_pkg_npm_repository_url(repository : String) -> String {
  if !repository.has_prefix("https://") {
    return repository
  }
  let git_url = "git+" + repository
  if git_url.has_suffix(".git") {
    git_url
  } else {
    git_url + ".git"
  }
}

///|
fn rewrite_mbt2ts_pkg_package_json(
  source : String,
  version : String,
  description : String?,
  license : String?,
  repository : String?,
  commands : Array[Mbt2tsPkgCommand],
) -> String? {
  let publish_paths = mbt2ts_pkg_collect_publish_paths(source, commands)
  if publish_paths.length() == 0 {
    return None
  }
  let lines : Array[String] = []
  let mut rewrote_version = false
  for line_view in source.split("\n") {
    let line = line_view.to_owned()
    if line.trim().has_prefix("\"version\":") {
      lines.push(
        "  \"version\": \"\{mbt2ts_pkg_escape_json_string(version)}\",",
      )
      match description {
        Some(value) =>
          lines.push(
            "  \"description\": \"\{mbt2ts_pkg_escape_json_string(value)}\",",
          )
        None => ()
      }
      match license {
        Some(value) =>
          lines.push(
            "  \"license\": \"\{mbt2ts_pkg_escape_json_string(value)}\",",
          )
        None => ()
      }
      match repository {
        Some(value) =>
          lines.push(
            "  \"repository\": { \"type\": \"git\", \"url\": \"\{mbt2ts_pkg_escape_json_string(mbt2ts_pkg_npm_repository_url(value))}\" },",
          )
        None => ()
      }
      if commands.length() > 0 {
        lines.push("  \"bin\": {")
        for idx in 0.. Bool {
  for name in candidate_names {
    let source = main_join_path(project_root, name)
    if main_file_exists(source) {
      return copy_binary_file(source, main_join_path(output_dir, name))
    }
  }
  true
}

///|
async fn copy_mbt2ts_pkg_publish_docs(
  project_root : String,
  output_dir : String,
) -> Bool {
  if !copy_mbt2ts_pkg_publish_file(project_root, output_dir, [
      "README.md", "README", "README.txt",
    ]) {
    return false
  }
  copy_mbt2ts_pkg_publish_file(project_root, output_dir, [
    "LICENSE", "LICENSE.md", "LICENSE.txt", "LICENCE", "LICENCE.md",
  ])
}

///|
/// Build the current MoonBit root package and write an npm publish directory.
///
/// This is deliberately project-oriented: it refreshes `pkg.generated.mbti`
/// from the module's `moon.mod`, sends the root source package through the
/// build-backed TypeScript scaffold, and then applies publish metadata owned
/// by the manifest. Low-level `--input` generation remains available for
/// tooling that already has an interface file.
async fn emit_typescript_npm_package_from_project(
  project_root : String,
  output_dir : String?,
  import_rewrite_path : String?,
  strict : Bool,
  facade : Bool,
) -> Bool {
  let module_root = @fs.realpath(project_root) catch { _ => project_root }
  let moon_mod_source = match read_moon_mod_source(module_root) {
    Some(source) => source
    None => {
      println("Error: --pkg could not read moon.mod from \{module_root}")
      return false
    }
  }
  let version = match parse_moon_mod_string_field(moon_mod_source, "version") {
    Some(version) if version != "" => version
    _ => {
      println("Error: --pkg requires a non-empty version in moon.mod")
      return false
    }
  }
  let description = parse_moon_mod_string_field(moon_mod_source, "description")
  let license = parse_moon_mod_string_field(moon_mod_source, "license")
  let repository = parse_moon_mod_string_field(moon_mod_source, "repository")
  let source_root_rel = match
    parse_moon_mod_string_field(moon_mod_source, "source") {
    Some(source) if source != "" => source
    _ => "."
  }
  let source_root = mbt2ts_pkg_source_root(module_root, moon_mod_source)
  let moon_pkg_path = main_join_path(source_root, "moon.pkg")
  if !main_file_exists(moon_pkg_path) {
    println(
      "Error: --pkg expects the module root package at \{moon_pkg_path}; add a root moon.pkg or use the lower-level --input flow.",
    )
    return false
  }
  let (info_exit, info_output) = @process.collect_output_merged(
    "moon",
    ["-C", module_root, "info"],
    cwd=".",
  )
  if info_exit != 0 {
    println("Error: moon info failed while preparing --pkg")
    println(info_output.text())
    return false
  }
  let mbti_path = main_join_path(source_root, "pkg.generated.mbti")
  if !main_file_exists(mbti_path) {
    println("Error: moon info did not generate \{mbti_path}")
    return false
  }
  let root_mbti_source = @fs.read_file(mbti_path).text() catch {
    e => {
      println("Error: --pkg could not read \{mbti_path}: \{e}")
      return false
    }
  }
  let root_package_name = match
    parse_mbti_package_name_from_source(root_mbti_source) {
    Some(name) => name
    None => {
      println(
        "Error: --pkg root interface has no package declaration: \{mbti_path}",
      )
      return false
    }
  }
  let submodule_names = match
    mbt2ts_pkg_collect_submodule_names(source_root, root_package_name) {
    Some(names) => names
    None => return false
  }
  let commands = match
    mbt2ts_pkg_collect_commands(
      source_root, root_package_name, source_root_rel, moon_mod_source,
    ) {
    Some(commands) => commands
    None => return false
  }
  let mut umbrella_mbti_path : String? = None
  let scaffold_mbti_path = if submodule_names.length() == 0 {
    mbti_path
  } else {
    let path = main_join_path(
      source_root, "__tsmbt_pkg_umbrella.generated.mbti",
    )
    if !write_text_file(
        path,
        mbt2ts_pkg_render_umbrella_mbti(root_mbti_source, submodule_names),
      ) {
      return false
    }
    umbrella_mbti_path = Some(path)
    path
  }
  let out = mbt2ts_pkg_output_dir(module_root, output_dir)
  let scaffold_ok = if facade {
    emit_typescript_facade_scaffold_from_mbti(
      scaffold_mbti_path, out, import_rewrite_path,
    )
  } else {
    emit_typescript_scaffold_from_mbti(
      scaffold_mbti_path, out, import_rewrite_path,
    )
  }
  match umbrella_mbti_path {
    Some(path) => {
      let _ = @fs.remove(path) catch { _ => () }
    }
    None => ()
  }
  if !scaffold_ok {
    return false
  }
  if !emit_mbt2ts_pkg_command_bins(module_root, source_root_rel, out, commands) {
    return false
  }
  let package_json_path = main_join_path(out, "package.json")
  let generated_package_json = @fs.read_file(package_json_path).text() catch {
    e => {
      println("Error: could not read generated package.json: \{e}")
      return false
    }
  }
  let publish_package_json = match
    rewrite_mbt2ts_pkg_package_json(
      generated_package_json, version, description, license, repository, commands,
    ) {
    Some(source) => source
    None => {
      println("Error: could not apply the moon.mod version to package.json")
      return false
    }
  }
  if !write_text_file(package_json_path, publish_package_json) {
    return false
  }
  if !copy_mbt2ts_pkg_publish_docs(module_root, out) {
    return false
  }
  let diagnostics_md = @fs.read_file(
    main_join_path(out, "AUTOLINK_DIAGNOSTICS.md"),
  ).text() catch {
    e => {
      println("Read error: \{e}")
      return false
    }
  }
  if strict && unified_autolink_diagnostics_has_omissions(diagnostics_md) {
    println(
      "Strict mode rejected MoonBit package: omitted autolink members detected.",
    )
    return false
  }
  println("Wrote publishable npm package to \{out}")
  true
}

///|
/// Run the unified `--input/--out/...` driver with a forced direction.
/// `print_help` is invoked on `--help`, missing required flags, or option
/// errors so each cmd module owns its own help banner.
async fn run_unified_cli_with_direction(
  args : Array[String],
  start : Int,
  forced_direction~ : TsmbtDirection,
  print_help~ : () -> Unit,
) -> Bool {
  let options = match parse_unified_tsmbt_options(args, start) {
    (Some(options), None) => options
    (_, Some("help")) => {
      print_help()
      return true
    }
    (_, Some(message)) => {
      println("Error: \{message}")
      print_help()
      return false
    }
    _ => {
      print_help()
      return false
    }
  }
  let direction = match (forced_direction, options.direction) {
    (TsmbtDirection::Auto, requested) => requested
    (TsmbtDirection::TsToMbt, TsmbtDirection::Auto)
    | (TsmbtDirection::TsToMbt, TsmbtDirection::TsToMbt) =>
      TsmbtDirection::TsToMbt
    (TsmbtDirection::MbtToTs, TsmbtDirection::Auto)
    | (TsmbtDirection::MbtToTs, TsmbtDirection::MbtToTs) =>
      TsmbtDirection::MbtToTs
    _ => {
      println(
        "Error: --direction conflicts with this command's fixed direction.",
      )
      return false
    }
  }
  let input = match options.input {
    Some(input) => input
    None => {
      println("Error: expected --input ")
      print_help()
      return false
    }
  }
  let out = match options.out {
    Some(out) => out
    None => {
      println("Error: expected --out ")
      print_help()
      return false
    }
  }
  emit_unified_tsmbt_scaffold(
    input,
    out,
    direction~,
    module_spec=options.module_spec,
    import_rewrite_path=options.import_rewrite_path,
    diagnostics_path=options.diagnostics_path,
    strict=options.strict,
    facade=options.facade,
  )
}

///|
/// Public entry: run the unified TS -> MoonBit scaffold driver with the
/// direction locked to `ts-to-mbt`. Used by `src/cmd/ts2mbt`.
pub async fn run_ts_to_mbt_unified_cli(
  args : Array[String],
  start : Int,
  print_help~ : () -> Unit,
) -> Bool {
  run_unified_cli_with_direction(
    args,
    start,
    forced_direction=TsmbtDirection::TsToMbt,
    print_help~,
  )
}

///|
/// Public entry: run the unified MoonBit -> TypeScript scaffold driver with
/// the direction locked to `mbt-to-ts`. Used by `src/cmd/mbt2ts`.
pub async fn run_mbt_to_ts_unified_cli(
  args : Array[String],
  start : Int,
  print_help~ : () -> Unit,
) -> Bool {
  run_unified_cli_with_direction(
    args,
    start,
    forced_direction=TsmbtDirection::MbtToTs,
    print_help~,
  )
}

///|
/// Public entry for `mbt2ts --pkg`. It discovers the enclosing MoonBit module
/// from the caller's working directory and leaves all package policy to the
/// project-level generator above.
pub async fn run_mbt_to_ts_pkg_cli(
  args : Array[String],
  start : Int,
  print_help~ : () -> Unit,
) -> Bool {
  let options = match parse_mbt2ts_pkg_options(args, start) {
    (Some(options), None) => options
    (_, Some("help")) => {
      print_help()
      return true
    }
    (_, Some(message)) => {
      println("Error: \{message}")
      print_help()
      return false
    }
    _ => {
      print_help()
      return false
    }
  }
  let cwd = @fs.realpath(".") catch { _ => "." }
  let module_root = match find_nearest_moon_mod_dir(cwd) {
    Some(root) => root
    None => {
      println(
        "Error: --pkg must run inside a MoonBit module containing moon.mod",
      )
      return false
    }
  }
  emit_typescript_npm_package_from_project(
    module_root,
    options.out,
    options.import_rewrite_path,
    options.strict,
    options.facade,
  )
}