///|
async fn main {
  let code = try {
    match @cli.arguments(@env.args()[1:]) {
      Err(message) => {
        @stdio.stderr.write(
          "wasm-devkit: " + message + "\nRun wasm-devkit --help for usage.\n",
        )
        2
      }
      Ok(Help) => {
        @stdio.stdout.write(@cli.help)
        0
      }
      Ok(Version) => {
        @stdio.stdout.write("wasm-devkit " + @cli.version() + "\n")
        0
      }
      Ok(Compare(options)) => {
        let left_data = if options.left == "-" {
          @stdio.stdin.read_all()
        } else {
          @fs.read_file(options.left)
        }
        let right_data = if options.right == "-" {
          @stdio.stdin.read_all()
        } else {
          @fs.read_file(options.right)
        }
        let left = @utf8.decode(left_data.binary(), ignore_bom=false)
        let right = @utf8.decode(right_data.binary(), ignore_bom=false)
        let result = @cli.compare(options, left, right)
        @stdio.stdout.write(result.stdout)
        @stdio.stderr.write(result.stderr)
        result.exit_code
      }
      Ok(Decompile(options)) => {
        let data = if options.path == "-" {
          @stdio.stdin.read_all()
        } else {
          @fs.read_file(options.path)
        }
        let input = data.binary()
        let result = @cli.decompile(options, input)
        // Only a complete document reaches the output file. An existing output
        // with the input's bytes can be the input under another path.
        if result.write_text is Some(text) && options.output is Some(path) {
          if options.path != "-" &&
            @fs.exists(path) &&
            @fs.read_file(path).binary() == input {
            @stdio.stderr.write(
              path + ": output holds the input binary; file was not written\n",
            )
            @sys.exit(2)
          }
          write_document(path, text)
        }
        if !result.stdout.is_empty() {
          @stdio.stdout.write(result.stdout)
        }
        if !result.stderr.is_empty() {
          @stdio.stderr.write(result.stderr)
        }
        result.exit_code
      }
      Ok(Inventory(options)) => {
        let data = if options.path == "-" {
          @stdio.stdin.read_all()
        } else {
          @fs.read_file(options.path)
        }
        let input = data.binary()
        let result = @cli.inventory(options, input)
        if result.write_text is Some(text) && options.output is Some(path) {
          if options.path != "-" &&
            @fs.exists(path) &&
            @fs.read_file(path).binary() == input {
            @stdio.stderr.write(
              path + ": output holds the input binary; file was not written\n",
            )
            @sys.exit(2)
          }
          write_document(path, text)
        }
        @stdio.stdout.write(result.stdout)
        @stdio.stderr.write(result.stderr)
        result.exit_code
      }
      Ok(Split(options)) => {
        let data = if options.path == "-" {
          @stdio.stdin.read_all()
        } else {
          @fs.read_file(options.path)
        }
        match
          @decompiler.split(
            data.binary(),
            select=options.select,
            source_id=options.path,
          ) {
          Err(errors) => {
            @stdio.stderr.write(@cli.render_split_diagnostics(errors))
            1
          }
          Ok(split) => {
            if !split.documents.is_empty() {
              let destination = options.directory
                .trim_end(chars="/\\")
                .to_owned()
              if destination.is_empty() || @fs.exists(destination) {
                @stdio.stderr.write(
                  options.directory +
                  ": split destination must be a new directory\n",
                )
                @sys.exit(2)
              }
              let unique = @fs.tmpdir(prefix="wasm-devkit-split")
              defer (@fs.rmdir(unique) catch { _ => () })
              let staged = destination +
                ".tmp-" +
                @registry_model.digest(@utf8.encode(unique))[7:].to_owned()
              @fs.mkdir(staged)
              defer (@fs.rmdir(staged, recursive=true) catch { _ => () })
              for document in split.documents {
                let path = staged + "/" + document.path
                let parts = path.split("/").to_array()
                let directory = parts[:parts.length() - 1]
                  .map(p => p.to_owned())
                  .join("/")
                @fs.mkdir(directory, recursive=true, allow_exist=true)
                @fs.write_file(path, document.text, create_mode=CreateNew)
              }
              @fs.rename(staged, destination, replace=false)
            }
            0
          }
        }
      }
      Ok(Run(options)) if options.command == Verify => {
        let result = @verification.execute(options)
        if !result.stdout.is_empty() {
          @stdio.stdout.write(result.stdout)
        }
        if !result.stderr.is_empty() {
          @stdio.stderr.write(result.stderr)
        }
        result.exit_code
      }
      Ok(Run(options)) => {
        let data = if options.path == "-" {
          @stdio.stdin.read_all()
        } else {
          @fs.read_file(options.path)
        }
        let source = @utf8.decode(data.binary(), ignore_bom=false)
        let result = @cli.execute(options, source)
        if result.write_text is Some(text) {
          // Do not overwrite a file changed since the read above.
          let current = @utf8.decode(
            @fs.read_file(options.path).binary(),
            ignore_bom=false,
          )
          if current != source {
            @stdio.stderr.write(
              options.path +
              ": changed while formatting; file was not written\n",
            )
            @sys.exit(2)
          }
          @fs.write_file(options.path, text)
        }
        if !result.stdout.is_empty() {
          @stdio.stdout.write(result.stdout)
        }
        if !result.stderr.is_empty() {
          @stdio.stderr.write(result.stderr)
        }
        result.exit_code
      }
    }
  } catch {
    error => {
      @stdio.stderr.write("wasm-devkit: " + error.to_string() + "\n")
      2
    }
  }
  if code != 0 {
    @sys.exit(code)
  }
}

///|
// Stage beside the destination so replacement is one filesystem operation.
// A failed write or rename cannot truncate an existing output document.
async fn write_document(path : String, text : String) -> Unit {
  let unique = @fs.tmpdir(prefix="wasm-devkit-document")
  defer (@fs.rmdir(unique) catch { _ => () })
  let staged = path +
    ".tmp-" +
    @registry_model.digest(@utf8.encode(unique))[7:].to_owned()
  defer (@fs.remove(staged) catch { _ => () })
  @fs.write_file(staged, text, create_mode=CreateNew)
  @fs.rename(staged, path, replace=true)
}