Add `bazel mod show_repo --output=streamed_proto` and `--output=streamed_jsonproto`

Serialize repo definitions to the same `Target` proto that `bazel query` uses.

A few more notable details:

*   Add back `_original_name`, even to the Starlark output. This was removed in https://github.com/bazelbuild/bazel/pull/26493, but I believe it's still useful for debugging.

*   The output protos may contain a `$apparent_repo_name` or `$module_key` pseudo-attribute, which is the equivalent of the `## @repo_name` / `## module@version` line in the Starlark output.

*   Similar to the original Starlark output, the same (canonical) repo can be shown multiple times if the user explicitly specified the same repo in different ways:

    ```sh
    ❯ bazel-bin/src/bazel_nojdk mod show_repo @@rules_cc+ @rules_cc rules_cc
    ```
    ```starlark
    ## @@rules_cc+:
    http_archive(
      name = "rules_cc+",
      ...
    ## @rules_cc:
    http_archive(
      name = "rules_cc+",
      ...
    ## rules_cc@0.2.9:
    http_archive(
      name = "rules_cc+",
      ...
    ```

    ```sh
    ❯ bazel-bin/src/bazel_nojdk mod show_repo --output=streamed_jsonproto @@rules_cc+ @rules_cc rules_cc
    ```
    ```js
    {"canonicalName":"rules_cc+", …
    {"canonicalName":"rules_cc+","apparentName":"@rules_cc", …
    {"canonicalName":"rules_cc+","moduleKey":"rules_cc@0.2.14", …
    ```

*   Tighten up command argument validation so that `bazel mod show_{repo,extension} --output={graph,json}` now exits with an error, addressing a common source of user confusion. I decided not to add the same validation to all `bazel mod` subcommands since no one can possibly expect `bazel mod tidy --output=graph` to do something.

Fixes #21617.

Works towards #24692.

Closes #27242.

PiperOrigin-RevId: 843878072
Change-Id: I275ad2d3d24472a85d875176374d29bf42397d16
diff --git a/docs/external/mod-command.mdx b/docs/external/mod-command.mdx
index 84086fb..122cd88 100644
--- a/docs/external/mod-command.mdx
+++ b/docs/external/mod-command.mdx
@@ -78,6 +78,8 @@
 The `<label_to_bzl_file>` part must be a repo-relative label (for example,
 `//pkg/path:file.bzl`).
 
+### Graph command options
+
 The following options only affect the subcommands that print graphs (`graph`,
 `deps`, `all_paths`, `path`, and `explain`):
 
@@ -138,7 +140,7 @@
     legacy platforms which cannot use Unicode.
 
 *   `--output <mode>`: Include information about the module extension usages as
-    part of the output graph. `<mode`> can be one of:
+    part of the output graph. `<mode>' can be one of:
 
     *   `text` *(default)*: A human-readable representation of the output graph
         (flattened as a tree).
@@ -155,6 +157,22 @@
     bazel mod graph --output graph | dot -Tsvg > /tmp/graph.svg
     ```
 
+    ### show_repo options
+`show_repo` supports a different set of output formats:
+*   `--output <mode>`: Change how repository definitions are displayed.
+    `<mode>` can be one of:
+    *   `text` *(default)*: Display repo definitions in Starlark.
+    *   `streamed_proto`: Prints a
+        [length-delimited](https://protobuf.dev/programming-guides/encoding/#size-limit)
+        stream of
+        [`Repository`](https://github.com/bazelbuild/bazel/blob/master/src/main/protobuf/build.proto)
+        protocol buffers.
+    *   `streamed_jsonproto`: Similar to `--output streamed_proto`, prints a
+        stream of [`Repository`](https://github.com/bazelbuild/bazel/blob/master/src/main/protobuf/build.proto)
+        protocol buffers but in [NDJSON](https://github.com/ndjson/ndjson-spec) format.
+
+        ### Other options
+
 Other options include:
 
 *   `--base_module <arg>` *default: `<root>`*: Specify a module relative to
diff --git a/site/en/external/mod-command.md b/site/en/external/mod-command.md
index 1e3556f..e086c46 100644
--- a/site/en/external/mod-command.md
+++ b/site/en/external/mod-command.md
@@ -82,6 +82,8 @@
 The `<label_to_bzl_file>` part must be a repo-relative label (for example,
 `//pkg/path:file.bzl`).
 
+### Graph command options
+
 The following options only affect the subcommands that print graphs (`graph`,
 `deps`, `all_paths`, `path`, and `explain`):
 
@@ -142,7 +144,7 @@
     legacy platforms which cannot use Unicode.
 
 *   `--output <mode>`: Include information about the module extension usages as
-    part of the output graph. `<mode`> can be one of:
+    part of the output graph. `<mode>` can be one of:
 
     *   `text` *(default)*: A human-readable representation of the output graph
         (flattened as a tree).
@@ -159,6 +161,31 @@
     bazel mod graph --output graph | dot -Tsvg > /tmp/graph.svg
     ```
 
+### show_repo options
+
+`show_repo` supports a different set of output formats:
+
+*   `--output <mode>`: Change how repository definitions are displayed.
+    `<mode>` can be one of:
+
+    *   `text` *(default)*: Display repository definitions in Starlark.
+
+    *   `streamed_proto`: Prints a
+        [length-delimited](https://protobuf.dev/programming-guides/encoding/#siz
+        e-limit)
+        stream of
+        [`Repository`](https://github.com/bazelbuild/bazel/blob/master/src/main/
+        protobuf/build.proto)
+        protocol buffers.
+
+    *   `streamed_jsonproto`: Similar to `--output streamed_proto`, prints a
+        stream of [`Repository`](https://github.com/bazelbuild/bazel/blob/master
+        /src/main/protobuf/build.proto)
+        protocol buffers but in [NDJSON](https://github.com/ndjson/ndjson-spec)
+        format.
+
+### Other options
+
 Other options include:
 
 *   `--base_module <arg>` *default: `<root>`*: Specify a module relative to
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/modcommand/BUILD b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/modcommand/BUILD
index 8c000b2..caa869c 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/modcommand/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/modcommand/BUILD
@@ -21,14 +21,20 @@
         "//src/main/java/com/google/devtools/build/lib/bazel/repository:repo_definition",
         "//src/main/java/com/google/devtools/build/lib/bazel/repository:repo_definition_value",
         "//src/main/java/com/google/devtools/build/lib/cmdline",
+        "//src/main/java/com/google/devtools/build/lib/packages",
+        "//src/main/java/com/google/devtools/build/lib/packages:label_printer",
         "//src/main/java/com/google/devtools/build/lib/util:maybe_complete_set",
+        "//src/main/java/com/google/devtools/build/lib/util:string_encoding",
         "//src/main/java/com/google/devtools/common/options",
         "//src/main/java/net/starlark/java/eval",
+        "//src/main/protobuf:build_java_proto",
         "//src/main/protobuf:failure_details_java_proto",
         "//third_party:auto_value",
         "//third_party:error_prone_annotations",
         "//third_party:gson",
         "//third_party:guava",
         "//third_party:jsr305",
+        "@com_google_protobuf//:protobuf_java",
+        "@com_google_protobuf//:protobuf_java_util",
     ],
 )
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/modcommand/ModExecutor.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/modcommand/ModExecutor.java
index b1125b5..dd0d108 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/modcommand/ModExecutor.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/modcommand/ModExecutor.java
@@ -17,6 +17,8 @@
 import static com.google.common.collect.ImmutableSet.toImmutableSet;
 import static com.google.common.collect.ImmutableSortedMap.toImmutableSortedMap;
 import static com.google.common.collect.ImmutableSortedSet.toImmutableSortedSet;
+import static java.nio.charset.StandardCharsets.US_ASCII;
+import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.Comparator.reverseOrder;
 import static java.util.Objects.requireNonNull;
 import static java.util.stream.Collectors.joining;
@@ -39,13 +41,13 @@
 import com.google.devtools.build.lib.bazel.bzlmod.modcommand.ModExecutor.ResultNode.IsExpanded;
 import com.google.devtools.build.lib.bazel.bzlmod.modcommand.ModExecutor.ResultNode.IsIndirect;
 import com.google.devtools.build.lib.bazel.bzlmod.modcommand.ModExecutor.ResultNode.NodeMetadata;
-import com.google.devtools.build.lib.bazel.repository.RepoDefinition;
 import com.google.devtools.build.lib.bazel.repository.RepoDefinitionValue;
-import com.google.devtools.build.lib.bazel.repository.RepoRule;
 import com.google.devtools.build.lib.util.MaybeCompleteSet;
 import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
 import java.io.PrintWriter;
-import java.io.Writer;
 import java.util.ArrayDeque;
 import java.util.Collections;
 import java.util.Comparator;
@@ -71,19 +73,22 @@
   private final ImmutableSetMultimap<ModuleExtensionId, String> extensionRepos;
   private final Optional<MaybeCompleteSet<ModuleExtensionId>> extensionFilter;
   private final ModOptions options;
+  private final OutputStream outputStream;
   private final PrintWriter printer;
   private ImmutableMap<ModuleExtensionId, ImmutableSetMultimap<String, ModuleKey>>
       extensionRepoImports;
 
   public ModExecutor(
-      ImmutableMap<ModuleKey, AugmentedModule> depGraph, ModOptions options, Writer writer) {
+      ImmutableMap<ModuleKey, AugmentedModule> depGraph,
+      ModOptions options,
+      OutputStream outputStream) {
     this(
         depGraph,
         ImmutableTable.of(),
         ImmutableSetMultimap.of(),
         Optional.of(MaybeCompleteSet.completeSet()),
         options,
-        writer);
+        outputStream);
   }
 
   public ModExecutor(
@@ -92,13 +97,17 @@
       ImmutableSetMultimap<ModuleExtensionId, String> extensionRepos,
       Optional<MaybeCompleteSet<ModuleExtensionId>> extensionFilter,
       ModOptions options,
-      Writer writer) {
+      OutputStream outputStream) {
     this.depGraph = depGraph;
     this.extensionUsages = extensionUsages;
     this.extensionRepos = extensionRepos;
     this.extensionFilter = extensionFilter;
     this.options = options;
-    this.printer = new PrintWriter(writer);
+    this.outputStream = outputStream;
+    this.printer =
+        new PrintWriter(
+            new OutputStreamWriter(
+                outputStream, options.charset == ModOptions.Charset.UTF8 ? UTF_8 : US_ASCII));
     // Easier lookup table for repo imports by module.
     // It is updated after pruneByDepthAndLink to filter out pruned modules.
     this.extensionRepoImports = computeRepoImportsTable(depGraph.keySet());
@@ -163,16 +172,15 @@
   }
 
   public void showRepo(ImmutableMap<String, RepoDefinitionValue> targetRepoDefinitions) {
+    var formatter = new RepoOutputFormatter(printer, outputStream, options.outputFormat);
     for (Map.Entry<String, RepoDefinitionValue> e : targetRepoDefinitions.entrySet()) {
-      if (e.getValue() instanceof RepoDefinitionValue.Found repoDefValue) {
-        printer.printf("## %s:\n", e.getKey());
-        printRepoDefinition(repoDefValue.repoDefinition());
-      }
-      if (e.getValue() instanceof RepoDefinitionValue.RepoOverride repoOverrideValue) {
-        printer.printf(
-            "## %s:\nBuiltin or overridden repo located at: %s\n\n",
-            e.getKey(), repoOverrideValue.repoPath());
-      }
+      formatter.print(e.getKey(), e.getValue());
+    }
+
+    try {
+      outputStream.flush();
+    } catch (IOException ex) {
+      // Ignore IOException like PrintWriter.
     }
     printer.flush();
   }
@@ -781,27 +789,4 @@
       abstract ResultNode build();
     }
   }
-
-  private void printRepoDefinition(RepoDefinition repoDefinition) {
-    RepoRule repoRule = repoDefinition.repoRule();
-    printer
-        .append("load(\"")
-        .append(repoRule.id().bzlFileLabel().getUnambiguousCanonicalForm())
-        .append("\", \"")
-        .append(repoRule.id().ruleName())
-        .append("\")\n");
-    printer.append(repoRule.id().ruleName()).append("(\n");
-    printer.append("  name = \"").append(repoDefinition.name()).append("\",\n");
-    for (Map.Entry<String, Object> attr : repoDefinition.attrValues().attributes().entrySet()) {
-      printer
-          .append("  ")
-          .append(attr.getKey())
-          .append(" = ")
-          .append(Starlark.repr(attr.getValue()))
-          .append(",\n");
-    }
-    printer.append(")\n");
-    // TODO: record and print the call stack for the repo definition itself?
-    printer.append("\n");
-  }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/modcommand/ModOptions.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/modcommand/ModOptions.java
index c7f9757..b7be841 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/modcommand/ModOptions.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/modcommand/ModOptions.java
@@ -266,9 +266,21 @@
 
   /** Possible formats of the `mod` command result. */
   public enum OutputFormat {
+    // Default
     TEXT,
+
+    // For graph commands:
     JSON,
-    GRAPH
+    GRAPH,
+
+    // For show_repo:
+    STREAMED_PROTO,
+    STREAMED_JSONPROTO;
+
+    @Override
+    public String toString() {
+      return Ascii.toLowerCase(this.name());
+    }
   }
 
   /** Converts an output format option string to a properly typed {@link OutputFormat} */
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/modcommand/OutputFormatters.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/modcommand/OutputFormatters.java
index 4f8170b..160fbaf 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/modcommand/OutputFormatters.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/modcommand/OutputFormatters.java
@@ -49,6 +49,7 @@
       case JSON -> jsonFormatter;
       case GRAPH -> graphvizFormatter;
       case null -> throw new IllegalArgumentException("Output format cannot be null.");
+      default -> throw new IllegalArgumentException("Unsupported output format: " + format);
     };
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/modcommand/RepoOutputFormatter.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/modcommand/RepoOutputFormatter.java
new file mode 100644
index 0000000..c71a90e
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/modcommand/RepoOutputFormatter.java
@@ -0,0 +1,170 @@
+// Copyright 2025 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.lib.bazel.bzlmod.modcommand;
+
+import static com.google.devtools.build.lib.util.StringEncoding.internalToUnicode;
+
+import com.google.devtools.build.lib.bazel.bzlmod.modcommand.ModOptions.OutputFormat;
+import com.google.devtools.build.lib.bazel.repository.RepoDefinition;
+import com.google.devtools.build.lib.bazel.repository.RepoDefinitionValue;
+import com.google.devtools.build.lib.bazel.repository.RepoRule;
+import com.google.devtools.build.lib.packages.Attribute;
+import com.google.devtools.build.lib.packages.AttributeFormatter;
+import com.google.devtools.build.lib.packages.LabelPrinter;
+import com.google.devtools.build.lib.query2.proto.proto2api.Build;
+import com.google.protobuf.InvalidProtocolBufferException;
+import com.google.protobuf.util.JsonFormat;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.util.Map;
+import net.starlark.java.eval.Starlark;
+
+/** Outputs repository definitions for {@code mod show_repo}. */
+public class RepoOutputFormatter {
+  private static final JsonFormat.Printer jsonPrinter =
+      JsonFormat.printer().omittingInsignificantWhitespace();
+
+  private final PrintWriter printer;
+  private final OutputStream outputStream;
+  private final OutputFormat outputFormat;
+
+  public RepoOutputFormatter(
+      PrintWriter printer, OutputStream outputStream, OutputFormat outputFormat) {
+    this.printer = printer;
+    this.outputStream = outputStream;
+    this.outputFormat = outputFormat;
+  }
+
+  public void print(String key, RepoDefinitionValue repoDefinition) {
+    switch (outputFormat) {
+      case TEXT -> printStarlark(key, repoDefinition);
+      case STREAMED_JSONPROTO, STREAMED_PROTO -> {
+        // In proto output formats, we only print repo definitions, not overrides.
+        if (repoDefinition instanceof RepoDefinitionValue.Found repoDefValue) {
+          if (outputFormat == OutputFormat.STREAMED_JSONPROTO) {
+            printProtoJson(key, repoDefValue.repoDefinition());
+          } else {
+            printStreamedProto(key, repoDefValue.repoDefinition());
+          }
+        }
+      }
+      default -> throw new IllegalArgumentException("Unknown output format: " + outputFormat);
+    }
+  }
+
+  private void printStarlark(String key, RepoDefinitionValue repoDefinition) {
+    if (repoDefinition instanceof RepoDefinitionValue.Found repoDefValue) {
+      printer.printf("## %s:\n", key);
+      printStarlark(repoDefValue.repoDefinition());
+    }
+    if (repoDefinition instanceof RepoDefinitionValue.RepoOverride repoOverrideValue) {
+      printer.printf(
+          "## %s:\nBuiltin or overridden repo located at: %s\n\n",
+          key, repoOverrideValue.repoPath());
+    }
+  }
+
+  private void printStarlark(RepoDefinition repoDefinition) {
+    RepoRule repoRule = repoDefinition.repoRule();
+    printer
+        .append("load(\"")
+        .append(repoRule.id().bzlFileLabel().getUnambiguousCanonicalForm())
+        .append("\", \"")
+        .append(repoRule.id().ruleName())
+        .append("\")\n");
+    printer.append(repoRule.id().ruleName()).append("(\n");
+    printer.append("  name = \"").append(repoDefinition.name()).append("\",\n");
+    if (repoDefinition.originalName() != null) {
+      printer.append("  _original_name = \"").append(repoDefinition.originalName()).append("\",\n");
+    }
+    for (Map.Entry<String, Object> attr : repoDefinition.attrValues().attributes().entrySet()) {
+      printer
+          .append("  ")
+          .append(attr.getKey())
+          .append(" = ")
+          .append(Starlark.repr(attr.getValue()))
+          .append(",\n");
+    }
+    printer.append(")\n");
+    // TODO: record and print the call stack for the repo definition itself?
+    printer.append("\n");
+  }
+
+  private void printStreamedProto(String key, RepoDefinition repoDefinition) {
+    Build.Repository serialized = serializeRepoDefinitionAsProto(key, repoDefinition);
+    try {
+      serialized.writeDelimitedTo(outputStream);
+    } catch (IOException e) {
+      // Ignore IOException like PrintWriter.
+    }
+  }
+
+  private void printProtoJson(String key, RepoDefinition repoDefinition) {
+    Build.Repository serialized = serializeRepoDefinitionAsProto(key, repoDefinition);
+    try {
+      printer.println(jsonPrinter.print(serialized));
+    } catch (InvalidProtocolBufferException e) {
+      throw new IllegalArgumentException(e);
+    }
+  }
+
+  private Build.Repository serializeRepoDefinitionAsProto(
+      String key, RepoDefinition repoDefinition) {
+    RepoRule repoRule = repoDefinition.repoRule();
+
+    Build.Repository.Builder pbBuilder = Build.Repository.newBuilder();
+    pbBuilder.setCanonicalName(internalToUnicode(repoDefinition.name()));
+    pbBuilder.setRepoRuleName(internalToUnicode(repoRule.id().ruleName()));
+    pbBuilder.setRepoRuleBzlLabel(
+        internalToUnicode(repoRule.id().bzlFileLabel().getUnambiguousCanonicalForm()));
+
+    // TODO: record and print the call stack for the repo definition itself?
+
+    if (key.startsWith("@")) {
+      if (!key.startsWith("@@")) {
+        pbBuilder.setApparentName(internalToUnicode(key));
+      }
+    } else {
+      pbBuilder.setModuleKey(internalToUnicode(key));
+    }
+    if (repoDefinition.originalName() != null) {
+      pbBuilder.setOriginalName(internalToUnicode(repoDefinition.originalName()));
+    }
+
+    for (Map.Entry<String, Integer> attr : repoRule.attributeIndices().entrySet()) {
+      String attrName = attr.getKey();
+      Attribute attrDefinition = repoRule.attributes().get(attr.getValue());
+
+      boolean explicitlySpecified = repoDefinition.attrValues().attributes().containsKey(attrName);
+      Object attrValue = repoDefinition.attrValues().attributes().get(attrName);
+      if (attrValue == null) {
+        attrValue = attrDefinition.getDefaultValueUnchecked();
+      }
+      Build.Attribute serializedAttribute =
+          AttributeFormatter.getAttributeProto(
+              attrDefinition,
+              attrValue,
+              explicitlySpecified,
+              /* encodeBooleanAndTriStateAsIntegerAndString= */ true,
+              /* sourceAspect= */ null,
+              /* includeAttributeSourceAspects= */ false,
+              LabelPrinter.legacy());
+      pbBuilder.addAttribute(serializedAttribute);
+    }
+
+    return pbBuilder.build();
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/commands/ModCommand.java b/src/main/java/com/google/devtools/build/lib/bazel/commands/ModCommand.java
index 2558e9a..90e6f12 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/commands/ModCommand.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/commands/ModCommand.java
@@ -141,7 +141,45 @@
   private void validateArgs(ModSubcommand subcommand, ModOptions modOptions, List<String> args)
       throws InvalidArgumentException {
 
-    // More validations can be added here in the future...
+    // Validate output format.
+    switch (subcommand) {
+      case SHOW_REPO -> {
+        switch (modOptions.outputFormat) {
+          case TEXT, STREAMED_JSONPROTO, STREAMED_PROTO -> {} // supported
+          default ->
+              throw new InvalidArgumentException(
+                  String.format(
+                      "Invalid --output '%s' for the 'show_repo' subcommand. Only 'text',"
+                          + " 'streamed_jsonproto', and 'streamed_proto' are supported.",
+                      modOptions.outputFormat),
+                  Code.INVALID_ARGUMENTS);
+        }
+      }
+      case SHOW_EXTENSION -> {
+        if (modOptions.outputFormat != ModOptions.OutputFormat.TEXT) {
+          throw new InvalidArgumentException(
+              String.format(
+                  "Invalid --output '%s' for the 'show_extension' subcommand. Only 'text' is"
+                      + " supported.",
+                  modOptions.outputFormat),
+              Code.INVALID_ARGUMENTS);
+        }
+      }
+      case ModSubcommand sub when sub.isGraph() -> {
+        switch (modOptions.outputFormat) {
+          case TEXT, JSON, GRAPH -> {} // supported
+          default ->
+              throw new InvalidArgumentException(
+                  String.format(
+                      "Invalid --output '%s' for the '%s' subcommand. "
+                          + "Only 'text', 'json', and 'graph' are supported.",
+                      modOptions.outputFormat, sub),
+                  Code.INVALID_ARGUMENTS);
+        }
+      }
+      // We don't validate other subcommands yet since they are less confusing.
+      default -> {}
+    }
 
     if (subcommand == ModSubcommand.SHOW_REPO) {
       int selectedModes = 0;
@@ -554,9 +592,7 @@
             moduleInspector.extensionToRepoInternalNames(),
             filterExtensions,
             modOptions,
-            new OutputStreamWriter(
-                env.getReporter().getOutErr().getOutputStream(),
-                modOptions.charset == UTF8 ? UTF_8 : US_ASCII));
+            env.getReporter().getOutErr().getOutputStream());
 
     try (SilentCloseable c =
         Profiler.instance().profile(ProfilerTask.BZLMOD, "execute mod " + subcommand)) {
diff --git a/src/main/protobuf/build.proto b/src/main/protobuf/build.proto
index 737375d..e665f5e 100644
--- a/src/main/protobuf/build.proto
+++ b/src/main/protobuf/build.proto
@@ -361,6 +361,32 @@
   optional stardoc_output.RuleInfo rule_class_info = 17;
 }
 
+// A Bazel repository, either representing a module, or created by a module
+// extension.
+message Repository {
+  // The canonical name of the repository.
+  optional string canonical_name = 1;
+
+  // The name of the repository rule (e.g., http_archive).
+  optional string repo_rule_name = 2;
+
+  // The canonical label of the bzl file that defined the repository rule.
+  optional string repo_rule_bzl_label = 3;
+
+  // The apparent name of the repository, as visible to --base_module.
+  optional string apparent_name = 4;
+
+  // If this repository is a module (not created by a module extension),
+  // this is the module key.
+  optional string module_key = 5;
+
+  // Original name in the repo as created by a module extension.
+  optional string original_name = 6;
+
+  // All of the attributes that describe the repository rule.
+  repeated Attribute attribute = 7;
+}
+
 // Direct dependencies of a rule in <depLabel, depConfiguration> form.
 message ConfiguredRuleInput {
   // Dep's target label.
diff --git a/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/modcommand/ModExecutorTest.java b/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/modcommand/ModExecutorTest.java
index 5a7d16d..9192340 100644
--- a/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/modcommand/ModExecutorTest.java
+++ b/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/modcommand/ModExecutorTest.java
@@ -18,7 +18,6 @@
 import static com.google.devtools.build.lib.bazel.bzlmod.BzlmodTestUtil.AugmentedModuleBuilder.buildAugmentedModule;
 import static com.google.devtools.build.lib.bazel.bzlmod.BzlmodTestUtil.buildTag;
 import static com.google.devtools.build.lib.bazel.bzlmod.BzlmodTestUtil.createModuleKey;
-import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.collect.ImmutableBiMap;
 import com.google.common.collect.ImmutableMap;
@@ -44,12 +43,11 @@
 import com.google.devtools.build.lib.cmdline.LabelSyntaxException;
 import com.google.devtools.build.lib.cmdline.PackageIdentifier;
 import com.google.devtools.build.lib.util.MaybeCompleteSet;
+import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.FileOutputStream;
 import java.io.IOException;
-import java.io.OutputStreamWriter;
-import java.io.StringWriter;
-import java.io.Writer;
+import java.io.OutputStream;
 import java.nio.file.Files;
 import java.util.List;
 import java.util.Optional;
@@ -65,7 +63,7 @@
   // TODO(andreisolo): Add a Json output test
   // TODO(andreisolo): Add a PATH query test
 
-  private final Writer writer = new StringWriter();
+  private final OutputStream outputStream = new ByteArrayOutputStream();
 
   // Tests for the ModExecutor::expandAndPrune core function.
   //
@@ -95,7 +93,7 @@
             .buildOrThrow();
 
     ModOptions options = ModOptions.getDefaultOptions();
-    ModExecutor executor = new ModExecutor(depGraph, options, writer);
+    ModExecutor executor = new ModExecutor(depGraph, options, outputStream);
 
     // RESULT:
     // <root> ...> ccc -> ddd
@@ -170,7 +168,7 @@
     ModOptions options = ModOptions.getDefaultOptions();
     options.cycles = true;
     options.depth = 1;
-    ModExecutor executor = new ModExecutor(depGraph, options, writer);
+    ModExecutor executor = new ModExecutor(depGraph, options, outputStream);
     ImmutableSet<ModuleKey> targets =
         ImmutableSet.of(createModuleKey("eee", "1.0"), createModuleKey("hhh", "1.0"));
 
@@ -246,7 +244,7 @@
     ModOptions options = ModOptions.getDefaultOptions();
     options.cycles = true;
     options.depth = 1;
-    ModExecutor executor = new ModExecutor(depGraph, options, writer);
+    ModExecutor executor = new ModExecutor(depGraph, options, outputStream);
     ImmutableSet<ModuleKey> targets = ImmutableSet.of(createModuleKey("eee", "1.0"));
 
     // RESULT:
@@ -464,9 +462,7 @@
 
     File file = File.createTempFile("output_text", "txt");
     file.deleteOnExit();
-    Writer writer = new OutputStreamWriter(new FileOutputStream(file), UTF_8);
 
-    ModExecutor executor = new ModExecutor(depGraph, options, writer);
     ImmutableSet<ModuleKey> targets =
         ImmutableSet.of(
             createModuleKey("C", "0.1"),
@@ -476,11 +472,16 @@
             createModuleKey("E", "1.0"),
             createModuleKey("H", "1.0"));
 
-    // Double check for human error
-    assertThat(executor.expandPathsToTargets(ImmutableSet.of(ModuleKey.ROOT), targets, false))
-        .isEqualTo(result);
+    try (var outputStream = new FileOutputStream(file)) {
+      ModExecutor executor = new ModExecutor(depGraph, options, outputStream);
 
-    executor.allPaths(ImmutableSet.of(ModuleKey.ROOT), targets);
+      // Double check for human error
+      assertThat(executor.expandPathsToTargets(ImmutableSet.of(ModuleKey.ROOT), targets, false))
+          .isEqualTo(result);
+
+      executor.allPaths(ImmutableSet.of(ModuleKey.ROOT), targets);
+    }
+
     List<String> textOutput = Files.readAllLines(file.toPath());
 
     assertThat(textOutput)
@@ -502,10 +503,10 @@
     options.outputFormat = OutputFormat.GRAPH;
     File fileGraph = File.createTempFile("output_graph", "txt");
     fileGraph.deleteOnExit();
-    writer = new OutputStreamWriter(new FileOutputStream(fileGraph), UTF_8);
-    executor = new ModExecutor(depGraph, options, writer);
-
-    executor.allPaths(ImmutableSet.of(ModuleKey.ROOT), targets);
+    try (var outputStream = new FileOutputStream(fileGraph)) {
+      var executor = new ModExecutor(depGraph, options, outputStream);
+      executor.allPaths(ImmutableSet.of(ModuleKey.ROOT), targets);
+    }
     List<String> graphOutput = Files.readAllLines(fileGraph.toPath());
 
     assertThat(graphOutput)
@@ -662,7 +663,6 @@
 
     File file = File.createTempFile("output_text", "txt");
     file.deleteOnExit();
-    Writer writer = new OutputStreamWriter(new FileOutputStream(file), UTF_8);
 
     // Contains the already-filtered map of target extensions along with their full list of repos
     ImmutableSetMultimap<ModuleExtensionId, String> extensionRepos =
@@ -675,11 +675,12 @@
     options.outputFormat = OutputFormat.TEXT;
     options.extensionInfo = ExtensionShow.ALL;
 
-    ModExecutor executor =
-        new ModExecutor(
-            depGraph, extensionUsages, extensionRepos, Optional.empty(), options, writer);
-
-    executor.graph(ImmutableSet.of(ModuleKey.ROOT));
+    try (var outputStream = new FileOutputStream(file)) {
+      ModExecutor executor =
+          new ModExecutor(
+              depGraph, extensionUsages, extensionRepos, Optional.empty(), options, outputStream);
+      executor.graph(ImmutableSet.of(ModuleKey.ROOT));
+    }
 
     List<String> textOutput = Files.readAllLines(file.toPath());
 
@@ -714,12 +715,13 @@
     options.outputFormat = OutputFormat.GRAPH;
     File fileGraph = File.createTempFile("output_graph", "txt");
     fileGraph.deleteOnExit();
-    writer = new OutputStreamWriter(new FileOutputStream(fileGraph), UTF_8);
-    executor =
-        new ModExecutor(
-            depGraph, extensionUsages, extensionRepos, Optional.empty(), options, writer);
 
-    executor.graph(ImmutableSet.of(ModuleKey.ROOT));
+    try (var outputStream = new FileOutputStream(fileGraph)) {
+      var executor =
+          new ModExecutor(
+              depGraph, extensionUsages, extensionRepos, Optional.empty(), options, outputStream);
+      executor.graph(ImmutableSet.of(ModuleKey.ROOT));
+    }
     List<String> graphOutput = Files.readAllLines(fileGraph.toPath());
 
     assertThat(graphOutput)
@@ -764,18 +766,19 @@
     options.depth = 1;
     File fileText2 = File.createTempFile("output_text2", "txt");
     fileText2.deleteOnExit();
-    writer = new OutputStreamWriter(new FileOutputStream(fileText2), UTF_8);
-    executor =
-        new ModExecutor(
-            depGraph,
-            extensionUsages,
-            extensionRepos,
-            Optional.of(MaybeCompleteSet.copyOf(ImmutableSet.of(mavenId))),
-            options,
-            writer);
+    try (var outputStream = new FileOutputStream(fileText2)) {
+      var executor =
+          new ModExecutor(
+              depGraph,
+              extensionUsages,
+              extensionRepos,
+              Optional.of(MaybeCompleteSet.copyOf(ImmutableSet.of(mavenId))),
+              options,
+              outputStream);
+      executor.allPaths(
+          ImmutableSet.of(ModuleKey.ROOT), ImmutableSet.of(createModuleKey("Y", "2.0")));
+    }
 
-    executor.allPaths(
-        ImmutableSet.of(ModuleKey.ROOT), ImmutableSet.of(createModuleKey("Y", "2.0")));
     List<String> textOutput2 = Files.readAllLines(fileText2.toPath());
 
     assertThat(textOutput2)
@@ -887,13 +890,13 @@
 
     File file = File.createTempFile("output_text", "txt");
     file.deleteOnExit();
-    Writer writer = new OutputStreamWriter(new FileOutputStream(file), UTF_8);
 
-    ModExecutor executor = new ModExecutor(depGraph, options, writer);
-    // Test `executor.allPaths`, it should output all "interesting" paths to the target modules.
-    executor.allPaths(
-        ImmutableSet.of(ModuleKey.ROOT), ImmutableSet.of(createModuleKey("D", "1.0")));
-    writer.close();
+    try (var outputStream = new FileOutputStream(file)) {
+      ModExecutor executor = new ModExecutor(depGraph, options, outputStream);
+      // Test `executor.allPaths`, it should output all "interesting" paths to the target modules.
+      executor.allPaths(
+          ImmutableSet.of(ModuleKey.ROOT), ImmutableSet.of(createModuleKey("D", "1.0")));
+    }
 
     List<String> textOutput = Files.readAllLines(file.toPath());
 
@@ -913,8 +916,8 @@
     // be the shortest one
     File file2 = File.createTempFile("output_text", "txt");
     file2.deleteOnExit();
-    try (Writer writer2 = new OutputStreamWriter(new FileOutputStream(file2), UTF_8)) {
-      ModExecutor executor2 = new ModExecutor(depGraph, options, writer2);
+    try (var outputStream2 = new FileOutputStream(file2)) {
+      ModExecutor executor2 = new ModExecutor(depGraph, options, outputStream2);
       executor2.path(ImmutableSet.of(ModuleKey.ROOT), ImmutableSet.of(createModuleKey("D", "1.0")));
     }
 
@@ -926,8 +929,8 @@
     // Test multiple targets D and G for allPaths
     File file3 = File.createTempFile("output_text_multi_all", "txt");
     file3.deleteOnExit();
-    try (Writer writer3 = new OutputStreamWriter(new FileOutputStream(file3), UTF_8)) {
-      ModExecutor executor3 = new ModExecutor(depGraph, options, writer3);
+    try (var outputStream3 = new FileOutputStream(file3)) {
+      ModExecutor executor3 = new ModExecutor(depGraph, options, outputStream3);
       executor3.allPaths(
           ImmutableSet.of(ModuleKey.ROOT),
           ImmutableSet.of(createModuleKey("D", "1.0"), createModuleKey("G", "1.0")));
@@ -950,8 +953,8 @@
     // Test multiple targets D and G for path (shortest path)
     File file4 = File.createTempFile("output_text_multi_path", "txt");
     file4.deleteOnExit();
-    try (Writer writer4 = new OutputStreamWriter(new FileOutputStream(file4), UTF_8)) {
-      ModExecutor executor4 = new ModExecutor(depGraph, options, writer4);
+    try (var outputStream4 = new FileOutputStream(file4)) {
+      ModExecutor executor4 = new ModExecutor(depGraph, options, outputStream4);
       executor4.path(
           ImmutableSet.of(ModuleKey.ROOT),
           ImmutableSet.of(createModuleKey("D", "1.0"), createModuleKey("G", "1.0")));
@@ -965,8 +968,8 @@
     // Test starting from E to D for allPaths
     File file5 = File.createTempFile("output_text_E_to_D_all", "txt");
     file5.deleteOnExit();
-    try (Writer writer5 = new OutputStreamWriter(new FileOutputStream(file5), UTF_8)) {
-      ModExecutor executor5 = new ModExecutor(depGraph, options, writer5);
+    try (var outputStream5 = new FileOutputStream(file5)) {
+      ModExecutor executor5 = new ModExecutor(depGraph, options, outputStream5);
       executor5.allPaths(
           ImmutableSet.of(createModuleKey("E", "1.0")),
           ImmutableSet.of(createModuleKey("D", "1.0")));
@@ -1012,8 +1015,8 @@
 
     File file = File.createTempFile("output_text_cycle", "txt");
     file.deleteOnExit();
-    try (Writer writer = new OutputStreamWriter(new FileOutputStream(file), UTF_8)) {
-      ModExecutor executor = new ModExecutor(depGraph, options, writer);
+    try (var outputStream = new FileOutputStream(file)) {
+      ModExecutor executor = new ModExecutor(depGraph, options, outputStream);
       executor.graph(ImmutableSet.of(ModuleKey.ROOT));
     }
 
@@ -1065,20 +1068,18 @@
 
     File file = File.createTempFile("output_text_repro", "txt");
     file.deleteOnExit();
-    Writer writer = new OutputStreamWriter(new FileOutputStream(file), UTF_8);
-
-    ModExecutor executor =
-        new ModExecutor(
-            depGraph,
-            extensionUsages,
-            extensionRepos,
-            Optional.of(MaybeCompleteSet.copyOf(ImmutableSet.of(mavenId))),
-            options,
-            writer);
-
-    // This should not throw NPE
-    executor.graph(ImmutableSet.of(ModuleKey.ROOT));
-    writer.close();
+    try (var outputStream = new FileOutputStream(file)) {
+      ModExecutor executor =
+          new ModExecutor(
+              depGraph,
+              extensionUsages,
+              extensionRepos,
+              Optional.of(MaybeCompleteSet.copyOf(ImmutableSet.of(mavenId))),
+              options,
+              outputStream);
+      // This should not throw NPE
+      executor.graph(ImmutableSet.of(ModuleKey.ROOT));
+    }
 
     List<String> textOutput = Files.readAllLines(file.toPath());
     assertThat(textOutput)
@@ -1159,7 +1160,7 @@
 
     File file = File.createTempFile("output_text_cycle_ext", "txt");
     file.deleteOnExit();
-    try (Writer writer = new OutputStreamWriter(new FileOutputStream(file), UTF_8)) {
+    try (var outputStream = new FileOutputStream(file)) {
       ModExecutor executor =
           new ModExecutor(
               depGraph,
@@ -1167,7 +1168,7 @@
               extensionRepos,
               Optional.of(MaybeCompleteSet.copyOf(ImmutableSet.of(extensionId))),
               options,
-              writer);
+              outputStream);
       executor.graph(ImmutableSet.of(ModuleKey.ROOT));
     }
 
diff --git a/src/test/py/bazel/bzlmod/mod_command_test.py b/src/test/py/bazel/bzlmod/mod_command_test.py
index 42c11cd..b10d7ce 100644
--- a/src/test/py/bazel/bzlmod/mod_command_test.py
+++ b/src/test/py/bazel/bzlmod/mod_command_test.py
@@ -504,6 +504,148 @@
         ],
     )
 
+  def testShowModuleAndExtensionReposFromBaseModuleJson(self):
+    _, stdout, _ = self.RunBazel(
+        [
+            'mod',
+            'show_repo',
+            '--base_module=foo@2.0',
+            '--output=streamed_jsonproto',
+            '@bar_from_foo2',
+            'ext@1.0',
+            '@my_repo3',
+            'bar',
+        ],
+        rstrip=True,
+    )
+    repos = [json.loads(line) for line in stdout]
+
+    ignored_attrs = {
+        'integrity',
+        'path',
+        'remote_module_file_urls',
+        'remote_module_file_integrity',
+        'urls',
+    }
+    for repo in repos:
+      attrs = repo.get('attribute')
+      if attrs:
+        repo['attribute'] = [
+            attr
+            for attr in attrs
+            if attr.get('explicitlySpecified', False)
+            and attr['name'] not in ignored_attrs
+        ]
+
+    self.assertListEqual(
+        repos,
+        [
+            {
+                'canonicalName': 'bar+',
+                'repoRuleName': 'http_archive',
+                'repoRuleBzlLabel': (
+                    '@@bazel_tools//tools/build_defs/repo:http.bzl'
+                ),
+                'apparentName': '@bar_from_foo2',
+                'attribute': [
+                    {
+                        'name': 'strip_prefix',
+                        'type': 'STRING',
+                        'stringValue': '',
+                        'explicitlySpecified': True,
+                        'nodep': False,
+                    },
+                    {
+                        'name': 'remote_file_urls',
+                        'type': 'STRING_LIST_DICT',
+                        'explicitlySpecified': True,
+                    },
+                    {
+                        'name': 'remote_file_integrity',
+                        'type': 'STRING_DICT',
+                        'explicitlySpecified': True,
+                    },
+                    {
+                        'name': 'remote_patches',
+                        'type': 'STRING_DICT',
+                        'explicitlySpecified': True,
+                    },
+                    {
+                        'name': 'remote_patch_strip',
+                        'type': 'INTEGER',
+                        'intValue': 0,
+                        'explicitlySpecified': True,
+                    },
+                ],
+            },
+            {
+                'canonicalName': 'ext+',
+                'repoRuleName': 'local_repository',
+                'repoRuleBzlLabel': (
+                    '@@bazel_tools//tools/build_defs/repo:local.bzl'
+                ),
+                'moduleKey': 'ext@1.0',
+                'attribute': [],
+            },
+            {
+                'canonicalName': 'ext++ext+repo3',
+                'repoRuleName': 'data_repo',
+                'repoRuleBzlLabel': '@@ext+//:ext.bzl',
+                'apparentName': '@my_repo3',
+                'originalName': 'repo3',
+                'attribute': [
+                    {
+                        'name': 'data',
+                        'type': 'STRING',
+                        'stringValue': 'requested repo',
+                        'nodep': False,
+                        'explicitlySpecified': True,
+                    },
+                ],
+            },
+            {
+                'canonicalName': 'bar+',
+                'repoRuleName': 'http_archive',
+                'repoRuleBzlLabel': (
+                    '@@bazel_tools//tools/build_defs/repo:http.bzl'
+                ),
+                'moduleKey': 'bar@2.0',
+                'attribute': [
+                    {
+                        'name': 'strip_prefix',
+                        'type': 'STRING',
+                        'stringValue': '',
+                        'explicitlySpecified': True,
+                        'nodep': False,
+                    },
+                    {
+                        'name': 'remote_file_urls',
+                        'type': 'STRING_LIST_DICT',
+                        'explicitlySpecified': True,
+                    },
+                    {
+                        'name': 'remote_file_integrity',
+                        'type': 'STRING_DICT',
+                        'explicitlySpecified': True,
+                    },
+                    {
+                        'name': 'remote_patches',
+                        'type': 'STRING_DICT',
+                        'explicitlySpecified': True,
+                    },
+                    {
+                        'name': 'remote_patch_strip',
+                        'type': 'INTEGER',
+                        'intValue': 0,
+                        'explicitlySpecified': True,
+                    },
+                ],
+            },
+        ],
+        'wrong output in the show query for module and extension-generated'
+        ' repos',
+    )
+
   def testShowModuleAndExtensionReposFromBaseModule(self):
     _, stdout, _ = self.RunBazel(
         [
@@ -526,11 +668,11 @@
     )
     self.assertRegex(stdout.pop(8), r'^  remote_module_file_integrity = ".*",$')
     self.assertRegex(stdout.pop(15), r'^  path = ".*",$')
-    self.assertRegex(stdout.pop(35), r'^  urls = \[".*"\],$')
-    self.assertRegex(stdout.pop(35), r'^  integrity = ".*",$')
-    self.assertRegex(stdout.pop(39), r'^  remote_module_file_urls = \[".*"\],$')
+    self.assertRegex(stdout.pop(37), r'^  urls = \[".*"\],$')
+    self.assertRegex(stdout.pop(37), r'^  integrity = ".*",$')
+    self.assertRegex(stdout.pop(41), r'^  remote_module_file_urls = \[".*"\],$')
     self.assertRegex(
-        stdout.pop(39), r'^  remote_module_file_integrity = ".*",$'
+        stdout.pop(41), r'^  remote_module_file_integrity = ".*",$'
     )
     self.assertListEqual(
         stdout,
@@ -567,6 +709,7 @@
             'load("@@ext+//:ext.bzl", "data_repo")',
             'data_repo(',
             '  name = "ext++ext+repo3",',
+            '  _original_name = "repo3",',
             '  data = "requested repo",',
             ')',
             '',
@@ -574,6 +717,7 @@
             'load("@@ext+//:ext.bzl", "data_repo")',
             'data_repo(',
             '  name = "ext++ext+repo4",',
+            '  _original_name = "repo4",',
             '  data = "requested repo",',
             ')',
             '',
@@ -584,14 +728,14 @@
             ),
             'http_archive(',
             '  name = "bar+",',
-            # pop(35) -- urls=[...]
-            # pop(35) -- integrity=...
+            # pop(37) -- urls=[...]
+            # pop(37) -- integrity=...
             '  strip_prefix = "",',
             '  remote_patches = {},',
             '  remote_file_urls = {},',
             '  remote_file_integrity = {},',
-            # pop(39) -- remote_module_file_urls=[...]
-            # pop(39) -- remote_module_file_integrity=...
+            # pop(41) -- remote_module_file_urls=[...]
+            # pop(41) -- remote_module_file_integrity=...
             '  remote_patch_strip = 0,',
             ')',
             '',
@@ -792,6 +936,16 @@
     self.assertIn('Builtin or overridden repo located at: ', stdout)
     self.assertIn('/embedded_tools', stdout)
 
+  def testShowRepoBazelToolsJson(self):
+    # @bazel_tools should be omitted from proto outputs
+    exit_code, stdout, stderr = self.RunBazel(
+        ['mod', 'show_repo', '--output=streamed_jsonproto', '@bazel_tools'],
+        rstrip=True,
+    )
+    self.AssertExitCode(exit_code, 0, stderr)
+    stdout = '\n'.join(stdout)
+    self.assertEqual('', stdout)
+
   def testDumpRepoMapping(self):
     _, stdout, _ = self.RunBazel(
         [