firmweaver: join CLI tools to new cmd firmweaver

- Top level cli "firmweaver coreboot_gpio" and
  "firmweaver zephyr_dtsi" (aliases: gpio, dtsi)
- Add unittests
- Update README.md with new build and usage instructions
This is needed so both tools can share authentication for google
sheets support and usage metrics.

BUG=b:475292981
TEST=go test -v -race ./...

Change-Id: Ie6293cf9f6c09e1c2650ae3e53de3da05989c0c8
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/firmweaver/+/8318028
Commit-Queue: Kshitij Shah <tij@google.com>
Reviewed-by: Karthik Ramasubramanian <kramasub@google.com>
Tested-by: Kshitij Shah <tij@google.com>
diff --git a/.gitignore b/.gitignore
index ca95764..4fdcb51 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,8 +1,6 @@
 # Binaries
-ec_fw/ec_fw
-ap_fw/gpio_tool
-
-# Go test binaries
+firmweaver
+cov.out
 *.test
 
 # IDEs
diff --git a/README.md b/README.md
index f97503a..3479249 100644
--- a/README.md
+++ b/README.md
@@ -18,47 +18,27 @@
 
 *   **Automatic Generation**: Generates firmware objects from hardware sheets.
 *   **Supported Outputs**:
-    *   EC Devicetree (`ec_fw`)
-    *   AP GPIO Configuration (`ap_fw`)
+    *   AP GPIO Configuration (`coreboot_gpio`, alias: `gpio`)
+    *   EC Devicetree (`zephyr_dtsi`, alias: `dtsi`)
 
 ## Getting Started
 
-### Building the GPIO Tool
+### Building Firmweaver
 
-The GPIO tool is located in the `ap_fw` directory.
+Build the `firmweaver` binary from the repository root:
 
-1. Navigate to the `ap_fw` directory:
-   ```bash
-   cd ap_fw
-   ```
-2. Build the tool:
-   ```bash
-   go build -o gpio_tool
-   ```
-   This will generate the `gpio_tool` executable in the `ap_fw` directory.
-
-### Building the EC Devicetree Generator
-
-The EC Devicetree generator is located in the `ec_fw` directory. See [`ec_fw/README.md`](ec_fw/README.md) for full details.
-
-1. Navigate to the `ec_fw` directory:
-   ```bash
-   cd ec_fw
-   ```
-2. Build the tool:
-   ```bash
-   go build -o ec_fw
-   ```
-   This will generate the `ec_fw` executable in the `ec_fw` directory.
+```bash
+go build -o firmweaver .
+```
 
 ## Usage
 
-### Running the GPIO Tool
+### Running the Coreboot GPIO Tool
 
-The `gpio_tool` parses a GPIO map CSV file and generates C configuration code.
+The `coreboot_gpio` (alias: `gpio`) parses a GPIO map CSV file and generates C configuration code.
 
 ```bash
-./ap_fw/gpio_tool --soc <soc_type> --csv <path_to_csv> [--output <path_to_output_c_file>]
+./firmweaver coreboot_gpio --soc <soc_type> --csv <path_to_csv> [--output <path_to_output_c_file>]
 ```
 
 #### Arguments:
@@ -69,22 +49,24 @@
 #### Example:
 To generate Intel GPIO configuration from the provided example CSV:
 ```bash
-./ap_fw/gpio_tool --soc intel --csv ap_fw/test/ExampleGpio.csv --output ap_fw/test/gpio_config.c
+./firmweaver coreboot_gpio --soc intel --csv coreboot_gpio/test/ExampleGpio.csv --output coreboot_gpio/test/gpio_config.c
 ```
+*(You can also use the shorthand `./firmweaver gpio ...`)*
 
-### Running the EC Devicetree Generator
+### Running the EC Zephyr Devicetree Generator
 
-The `ec_fw` tool parses an EC pinout CSV spreadsheet and generates Zephyr devicetree overlays.
+The `zephyr_dtsi` (alias: `dtsi`) parses an EC pinout CSV spreadsheet and generates Zephyr devicetree overlays.
 
 ```bash
-./ec_fw/ec_fw --chip <chip_name> --csv <path_to_csv> [--pin-col <variant>] [--output <path_to_output_dtsi>]
+./firmweaver zephyr_dtsi --chip <chip_name> --csv <path_to_csv> [--pin-col <variant>] [--output <path_to_output_dtsi>]
 ```
 
 #### Example:
 To generate Nuvoton NPCX9 Devicetree overlay from the provided example CSV:
 ```bash
-./ec_fw/ec_fw --chip npcx99 --csv ec_fw/test/ExampleEcPinout.csv --pin-col "NPCX99 (NPCX)" --output ec_fw/test/generated.dtsi
+./firmweaver zephyr_dtsi --chip npcx99 --csv zephyr_dtsi/test/ExampleEcPinout.csv --pin-col "NPCX99 (NPCX)" --output zephyr_dtsi/test/generated.dtsi
 ```
+*(You can also use the shorthand `./firmweaver dtsi ...`)*
 
 ## Testing
 
diff --git a/coreboot_gpio/gpio_tool.go b/coreboot_gpio/gpio_tool.go
index f9f5597..4a54411 100644
--- a/coreboot_gpio/gpio_tool.go
+++ b/coreboot_gpio/gpio_tool.go
@@ -2,15 +2,17 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-package main
+package coreboot_gpio
 
 import (
 	"encoding/csv"
+	"errors"
 	"flag"
 	"fmt"
 	"io"
 	"net/url"
 	"os"
+	"path/filepath"
 	"strconv"
 	"strings"
 )
@@ -75,42 +77,35 @@
 	return gpioPins, nil
 }
 
-func main() {
-	soc := flag.String("soc", "", "SoC type (intel, qcom, or mtk)")
-	csvFile := flag.String("csv", "", "Local file path for GPIO map CSV")
-	outputFile := flag.String("output", "", "Output file path")
-	flag.Parse()
+// Run executes the AP firmware GPIO configuration generation logic.
+func Run(args []string) error {
+	fs := flag.NewFlagSet("coreboot_gpio", flag.ContinueOnError)
+	soc := fs.String("soc", "", "SoC type (intel, qcom, or mtk)")
+	csvFile := fs.String("csv", "", "Local file path for GPIO map CSV")
+	outputFile := fs.String("output", "", "Output file path")
+
+	if err := fs.Parse(args); err != nil {
+		if errors.Is(err, flag.ErrHelp) {
+			return nil
+		}
+		return err
+	}
+	if len(fs.Args()) > 0 {
+		return fmt.Errorf("unexpected arguments: %v", fs.Args())
+	}
 
 	if *soc == "" {
-		fmt.Fprintln(os.Stderr, "Error: --soc flag is required")
-		flag.Usage()
-		os.Exit(1)
+		return fmt.Errorf("--soc flag is required")
 	}
 
 	if *csvFile == "" {
-		fmt.Fprintln(os.Stderr, "Error: --csv flag is required")
-		flag.Usage()
-		os.Exit(1)
-	}
-
-	var outputWriter io.Writer
-	if *outputFile != "" {
-		f, err := os.Create(*outputFile)
-		if err != nil {
-			fmt.Fprintf(os.Stderr, "Error creating output file: %v\n", err)
-			os.Exit(1)
-		}
-		defer f.Close()
-		outputWriter = f
-	} else {
-		outputWriter = os.Stdout
+		return fmt.Errorf("--csv flag is required")
 	}
 
 	// Open the local CSV file
 	file, err := os.Open(*csvFile)
 	if err != nil {
-		fmt.Fprintf(os.Stderr, "Error opening CSV file: %v\n", err)
-		os.Exit(1)
+		return fmt.Errorf("opening CSV file: %w", err)
 	}
 	defer file.Close()
 
@@ -118,36 +113,63 @@
 	fmt.Fprintf(os.Stderr, "Reading GPIO map from: %s\n", *csvFile)
 
 	reader := csv.NewReader(file)
+	reader.FieldsPerRecord = -1
 	records, err := reader.ReadAll()
 	if err != nil {
-		fmt.Fprintf(os.Stderr, "Error parsing CSV: %v\n", err)
-		os.Exit(1)
+		return fmt.Errorf("parsing CSV: %w", err)
 	}
 
+	var outputWriter io.Writer
+	var outCloser io.Closer
+	if *outputFile != "" {
+		dir := filepath.Dir(*outputFile)
+		if err := os.MkdirAll(dir, 0755); err != nil {
+			return fmt.Errorf("creating output directory: %w", err)
+		}
+		f, err := os.Create(*outputFile)
+		if err != nil {
+			return fmt.Errorf("creating output file: %w", err)
+		}
+		outCloser = f
+		outputWriter = f
+	} else {
+		outputWriter = os.Stdout
+	}
+	defer func() {
+		if outCloser != nil {
+			_ = outCloser.Close()
+		}
+	}()
+
 	switch *soc {
 	case "intel":
 		err := processIntelGPIOs(records, outputWriter)
 		if err != nil {
-			fmt.Fprintf(os.Stderr, "Error processing Intel GPIOs: %v\n", err)
-			os.Exit(1)
+			return fmt.Errorf("processing Intel GPIOs: %w", err)
 		}
 	case "qcom", "mtk":
 		gpioPins, err := parseGenericCSV(records)
 		if err != nil {
-			fmt.Fprintf(os.Stderr, "Error parsing CSV: %v\n", err)
-			os.Exit(1)
+			return fmt.Errorf("parsing CSV: %w", err)
 		}
 		fmt.Fprintln(os.Stderr, "Successfully parsed GPIO pin data.")
 		for _, pin := range gpioPins {
 			fmt.Fprintf(outputWriter, "- Name: %s, Pin: %d, Function: %s\n", pin.Name, pin.Pin, pin.Function)
 		}
 	default:
-		fmt.Fprintf(os.Stderr, "Error: Unsupported SoC '%s'. Must be one of 'intel', 'qcom', or 'mtk'.\n", *soc)
-		flag.Usage()
-		os.Exit(1)
+		return fmt.Errorf("unsupported SoC '%s'. Must be one of 'intel', 'qcom', or 'mtk'", *soc)
+	}
+
+	if outCloser != nil {
+		if err := outCloser.Close(); err != nil {
+			return fmt.Errorf("closing output file: %w", err)
+		}
+		outCloser = nil
 	}
 
 	if *outputFile != "" {
 		fmt.Fprintf(os.Stderr, "Parsed data written to %s\n", *outputFile)
 	}
+
+	return nil
 }
diff --git a/coreboot_gpio/gpio_tool_test.go b/coreboot_gpio/gpio_tool_test.go
new file mode 100644
index 0000000..a3711d6
--- /dev/null
+++ b/coreboot_gpio/gpio_tool_test.go
@@ -0,0 +1,51 @@
+// Copyright 2026 The ChromiumOS Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package coreboot_gpio
+
+import (
+	"os"
+	"path/filepath"
+	"strings"
+	"testing"
+)
+
+func TestGpioTool_RunValidation(t *testing.T) {
+	if err := Run([]string{"-h"}); err != nil {
+		t.Errorf("Run('-h') unexpected error: %v", err)
+	}
+	if err := Run([]string{"--help"}); err != nil {
+		t.Errorf("Run(--help) expected nil, got %v", err)
+	}
+	if err := Run([]string{}); err == nil {
+		t.Errorf("expected error with no flags")
+	}
+	if err := Run([]string{"--soc=intel"}); err == nil {
+		t.Errorf("expected error without --csv")
+	}
+	if err := Run([]string{"--soc=unsupported", "--csv=test/ExampleGpio.csv"}); err == nil {
+		t.Errorf("expected error with unsupported SoC")
+	}
+}
+
+func TestGpioTool_RunSuccess(t *testing.T) {
+	out := filepath.Join(t.TempDir(), "gpio.c")
+	err := Run([]string{
+		"--soc=intel",
+		"--csv=test/ExampleGpio.csv",
+		"--output=" + out,
+	})
+	if err != nil {
+		t.Fatalf("Run failed: %v", err)
+	}
+
+	data, err := os.ReadFile(out)
+	if err != nil {
+		t.Fatalf("failed to read output file: %v", err)
+	}
+	content := string(data)
+	if !strings.Contains(content, "PAD_CFG") {
+		t.Errorf("expected generated C file to contain PAD_CFG")
+	}
+}
diff --git a/coreboot_gpio/intel_gpio_config.go b/coreboot_gpio/intel_gpio_config.go
index 936f1c2..f5a02d3 100644
--- a/coreboot_gpio/intel_gpio_config.go
+++ b/coreboot_gpio/intel_gpio_config.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-package main
+package coreboot_gpio
 
 import (
 	"fmt"
@@ -190,7 +190,7 @@
 
 // writeCBoilerplate writes the header of the generated C file.
 func writeCBoilerplate(w io.Writer) {
-	fmt.Fprintln(w, "// Generated by gpio_tool.go")
+	fmt.Fprintln(w, "// Generated by coreboot_gpio")
 	fmt.Fprintln(w, "")
 	fmt.Fprintln(w, "static const struct pad_config gpio_table[] = {")
 }
diff --git a/coreboot_gpio/test/gpio_config.c b/coreboot_gpio/test/gpio_config.c
index 4a5fe3e..8846b87 100644
--- a/coreboot_gpio/test/gpio_config.c
+++ b/coreboot_gpio/test/gpio_config.c
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-// Generated by gpio_tool.go
+// Generated by coreboot_gpio
 
 static const struct pad_config gpio_table[] = {
     /* GPP_A02:     ESPI_SOC_IO2_R */
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..596f77b
--- /dev/null
+++ b/main.go
@@ -0,0 +1,65 @@
+// Copyright 2026 The ChromiumOS Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package main
+
+import (
+	"errors"
+	"flag"
+	"fmt"
+	"io"
+	"os"
+
+	"firmweaver/coreboot_gpio"
+	"firmweaver/zephyr_dtsi"
+)
+
+func printUsage(w io.Writer) {
+	fmt.Fprintln(w, `Firmweaver
+
+Usage:
+  firmweaver <command> [flags...]
+
+Available Commands:
+  coreboot_gpio (alias: gpio)    Generate AP SoC GPIO configurations
+  zephyr_dtsi   (alias: dtsi)    Generate EC Devicetree (DTSI) pinmux configurations`)
+}
+
+func main() {
+	if err := run(os.Args[1:]); err != nil {
+		if errors.Is(err, flag.ErrHelp) {
+			os.Exit(0)
+		}
+		fmt.Fprintf(os.Stderr, "Error: %v\n", err)
+		os.Exit(1)
+	}
+}
+
+func run(args []string) error {
+	if len(args) == 0 {
+		printUsage(os.Stderr)
+		return fmt.Errorf("subcommand required: 'coreboot_gpio' or 'zephyr_dtsi'")
+	}
+
+	switch args[0] {
+	case "coreboot_gpio", "gpio":
+		err := coreboot_gpio.Run(args[1:])
+		if errors.Is(err, flag.ErrHelp) {
+			return nil
+		}
+		return err
+	case "zephyr_dtsi", "dtsi":
+		err := zephyr_dtsi.Run(args[1:])
+		if errors.Is(err, flag.ErrHelp) {
+			return nil
+		}
+		return err
+	case "-h", "--help", "help":
+		printUsage(os.Stdout)
+		return nil
+	default:
+		printUsage(os.Stderr)
+		return fmt.Errorf("unknown command %q", args[0])
+	}
+}
diff --git a/main_test.go b/main_test.go
new file mode 100644
index 0000000..888a40d
--- /dev/null
+++ b/main_test.go
@@ -0,0 +1,45 @@
+// Copyright 2026 The ChromiumOS Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package main
+
+import (
+	"testing"
+)
+
+func TestRun_SubcommandRouting(t *testing.T) {
+	if err := run([]string{}); err == nil {
+		t.Error("run([]) expected error for missing subcommand, got nil")
+	}
+	if err := run([]string{"help"}); err != nil {
+		t.Errorf("run('help') unexpected error: %v", err)
+	}
+	if err := run([]string{"-h"}); err != nil {
+		t.Errorf("run('-h') unexpected error: %v", err)
+	}
+	if err := run([]string{"--help"}); err != nil {
+		t.Errorf("run('--help') unexpected error: %v", err)
+	}
+	if err := run([]string{"zephyr_dtsi", "-h"}); err != nil {
+		t.Errorf("run('zephyr_dtsi -h') unexpected error: %v", err)
+	}
+	if err := run([]string{"zephyr_dtsi", "--help"}); err != nil {
+		t.Errorf("run('zephyr_dtsi --help') unexpected error: %v", err)
+	}
+	if err := run([]string{"dtsi", "-h"}); err != nil {
+		t.Errorf("run('dtsi -h') unexpected error: %v", err)
+	}
+	if err := run([]string{"coreboot_gpio", "-h"}); err != nil {
+		t.Errorf("run('coreboot_gpio -h') unexpected error: %v", err)
+	}
+	if err := run([]string{"coreboot_gpio", "--help"}); err != nil {
+		t.Errorf("run('coreboot_gpio --help') unexpected error: %v", err)
+	}
+	if err := run([]string{"gpio", "-h"}); err != nil {
+		t.Errorf("run('gpio -h') unexpected error: %v", err)
+	}
+	if err := run([]string{"invalid"}); err == nil {
+		t.Error("run('invalid') expected error for unknown command, got nil")
+	}
+}
diff --git a/zephyr_dtsi/README.md b/zephyr_dtsi/README.md
index 4bcbf4f..0dd96e5 100644
--- a/zephyr_dtsi/README.md
+++ b/zephyr_dtsi/README.md
@@ -1,6 +1,6 @@
-# EC Firmware Devicetree Generator (`ec_fw`)
+# EC Firmware Devicetree Generator (`zephyr_dtsi`)
 
-`ec_fw` is a tool for generating Zephyr EC Devicetree configuration files (`generated.dtsi`) for Chromium OS EC board bringup.
+`zephyr_dtsi` is a tool for generating Zephyr EC Devicetree configuration files (`generated.dtsi`) for Chromium OS EC board bringup.
 
 It ingests hardware pinout CSV spreadsheets prepared by board design teams and outputs a self-contained Zephyr Devicetree overlay containing named GPIOs, ADC channels, PWM channels, I2C ports, TACH channels, and Keyboard matrix configurations.
 
@@ -28,7 +28,7 @@
 ## CLI Reference
 
 ```bash
-go run ./ec_fw --chip=<chip_name> --csv=<path_to_csv> [options]
+./firmweaver dtsi --chip=<chip_name> --csv=<path_to_csv> [options]
 ```
 
 ### Options
@@ -46,29 +46,29 @@
 ## Usage Examples
 
 ### 1. Generating Devicetree Using the Bundled Test Example
-To test generating a `generated.dtsi` file from the example test CSV included in `ec_fw/test/`:
+To test generating a `generated.dtsi` file from the example test CSV included in `zephyr_dtsi/test/`:
 
 For NPCX (using `npcx99` chip):
 ```bash
-go run ./ec_fw --chip=npcx99 \
-               --csv=ec_fw/test/ExampleEcPinout.csv \
+./firmweaver dtsi --chip=npcx99 \
+               --csv=zephyr_dtsi/test/ExampleEcPinout.csv \
                --pin-col="NPCX99 (NPCX)" \
-               --output=ec_fw/test/generated_npcx.dtsi
+               --output=zephyr_dtsi/test/generated_npcx.dtsi
 ```
 
 For ITE (using `it82002` chip):
 ```bash
-go run ./ec_fw --chip=it82002 \
-               --csv=ec_fw/test/ExampleEcPinout.csv \
+./firmweaver dtsi --chip=it82002 \
+               --csv=zephyr_dtsi/test/ExampleEcPinout.csv \
                --pin-col="IT82002 (ITE)" \
-               --output=ec_fw/test/generated_ite.dtsi
+               --output=zephyr_dtsi/test/generated_ite.dtsi
 ```
 
 ### 2. Generating Devicetree for Board Variants
 To generate a `generated.dtsi` file for the Fatcat **RVP** (NPCX chip):
 
 ```bash
-go run ./ec_fw --chip=npcx99 \
+./firmweaver dtsi --chip=npcx99 \
                --csv=/path/to/fatcat.csv \
                --pin-col="NPCx9 RVP" \
                --output=/path/to/platform/ec/zephyr/program/fatcat/generated_npcx.dtsi
@@ -77,18 +77,18 @@
 To generate for the Fatcat **ITE** variant:
 
 ```bash
-go run ./ec_fw --chip=it82002 \
+./firmweaver dtsi --chip=it82002 \
                --csv=/path/to/fatcat.csv \
                --pin-col="IT82002 Ref" \
                --output=/path/to/platform/ec/zephyr/program/fatcat/generated_ite.dtsi
 ```
 
 ### 3. Building and Running Binary
-Build the standalone `ec_fw` binary from the repository root:
+Build the standalone `zephyr_dtsi` binary from the repository root:
 
 ```bash
-go build -o ec_fw ./ec_fw
-./ec_fw --chip=it82202ax --csv=/path/to/hylia.csv --output=generated.dtsi
+go build -o firmweaver .
+./firmweaver dtsi --chip=it82202ax --csv=/path/to/hylia.csv --output=generated.dtsi
 ```
 
 ### 4. Adding Custom Chip Pin Maps
@@ -106,12 +106,12 @@
    ```
 2. Pass the directory containing your JSON file to `--chip-data-dir`:
    ```bash
-   go run ./ec_fw --chip-data-dir=/path/to/custom_chips --chip=custom_chip --csv=board.csv
+   ./firmweaver dtsi --chip-data-dir=/path/to/custom_chips --chip=custom_chip --csv=board.csv
    ```
 
 ### 5. Running Tests
-Run unit tests across the `ec_fw` tool:
+Run unit tests across the `zephyr_dtsi` tool:
 
 ```bash
-go test -v ./ec_fw/...
+go test -v ./zephyr_dtsi/...
 ```
diff --git a/zephyr_dtsi/chips.go b/zephyr_dtsi/chips.go
index 7007069..a9cf881 100644
--- a/zephyr_dtsi/chips.go
+++ b/zephyr_dtsi/chips.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-package main
+package zephyr_dtsi
 
 import (
 	"embed"
diff --git a/zephyr_dtsi/chips_test.go b/zephyr_dtsi/chips_test.go
index 3fca81d..f2db0b2 100644
--- a/zephyr_dtsi/chips_test.go
+++ b/zephyr_dtsi/chips_test.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-package main
+package zephyr_dtsi
 
 import (
 	"os"
diff --git a/zephyr_dtsi/generator.go b/zephyr_dtsi/generator.go
index f7dd536..030ef17 100644
--- a/zephyr_dtsi/generator.go
+++ b/zephyr_dtsi/generator.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-package main
+package zephyr_dtsi
 
 import (
 	"cmp"
diff --git a/zephyr_dtsi/generator_test.go b/zephyr_dtsi/generator_test.go
index 629b109..aa304cc 100644
--- a/zephyr_dtsi/generator_test.go
+++ b/zephyr_dtsi/generator_test.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-package main
+package zephyr_dtsi
 
 import (
 	"bytes"
diff --git a/zephyr_dtsi/main.go b/zephyr_dtsi/main.go
deleted file mode 100644
index 0846fa8..0000000
--- a/zephyr_dtsi/main.go
+++ /dev/null
@@ -1,90 +0,0 @@
-// Copyright 2026 The ChromiumOS Authors
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-package main
-
-import (
-	"flag"
-	"fmt"
-	"io"
-	"os"
-	"path/filepath"
-)
-
-func main() {
-	if err := run(); err != nil {
-		fmt.Fprintf(os.Stderr, "%v\n", err)
-		os.Exit(1)
-	}
-}
-
-func run() error {
-	chipFlag := flag.String("chip", "", "Supported EC chip name or alias (e.g. npcx99, npcx993, it81302, it81202, it82002, it82202)")
-	csvFlag := flag.String("csv", "", "Path to the EC pinout CSV file")
-	pinColFlag := flag.String("pin-col", "", "Optional: Column header containing pin numbers (e.g. 'NPCX99 (NPCX)' or 'IT82002 (ITE)')")
-	chipDataDirFlag := flag.String("chip-data-dir", "", "Optional: Path to directory containing custom chip JSON data files")
-	outputFlag := flag.String("output", "", "Path to the output DTSI file (defaults to stdout)")
-	flag.Parse()
-
-	if *chipDataDirFlag != "" {
-		if err := LoadExternalChipDir(*chipDataDirFlag); err != nil {
-			return fmt.Errorf("error loading chip data dir %s: %w", *chipDataDirFlag, err)
-		}
-	}
-
-	if *chipFlag == "" {
-		flag.Usage()
-		return fmt.Errorf("error: --chip is required. Available chips: %s", GetAvailableChips())
-	}
-
-	if *csvFlag == "" {
-		flag.Usage()
-		return fmt.Errorf("error: --csv is required")
-	}
-
-	chip, ok := GetChip(*chipFlag)
-	if !ok {
-		return fmt.Errorf("error: Unsupported chip %q. Available: %s", *chipFlag, GetAvailableChips())
-	}
-
-	csvFile, err := os.Open(*csvFlag)
-	if err != nil {
-		return fmt.Errorf("error opening CSV file: %w", err)
-	}
-	defer csvFile.Close()
-
-	configs, err := ParseCSV(csvFile, *pinColFlag)
-	if err != nil {
-		return fmt.Errorf("error parsing CSV: %w", err)
-	}
-
-	var out io.Writer = os.Stdout
-	var tmpFile *os.File
-	if *outputFlag != "" {
-		dir := filepath.Dir(*outputFlag)
-		var err error
-		tmpFile, err = os.CreateTemp(dir, ".tmp_generated_*.dtsi")
-		if err != nil {
-			return fmt.Errorf("error creating temp file: %w", err)
-		}
-		defer func() {
-			tmpFile.Close()
-			os.Remove(tmpFile.Name())
-		}()
-		out = tmpFile
-	}
-
-	if err := GenerateDTSI(out, configs, chip); err != nil {
-		return fmt.Errorf("error generating DTSI: %w", err)
-	}
-
-	if tmpFile != nil {
-		tmpFile.Close()
-		if err := os.Rename(tmpFile.Name(), *outputFlag); err != nil {
-			return fmt.Errorf("error saving output file: %w", err)
-		}
-	}
-
-	return nil
-}
diff --git a/zephyr_dtsi/main_test.go b/zephyr_dtsi/main_test.go
index c598e4c..c0e7f29 100644
--- a/zephyr_dtsi/main_test.go
+++ b/zephyr_dtsi/main_test.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-package main
+package zephyr_dtsi
 
 import (
 	"slices"
diff --git a/zephyr_dtsi/parser.go b/zephyr_dtsi/parser.go
index d91d6d2..6e9327e 100644
--- a/zephyr_dtsi/parser.go
+++ b/zephyr_dtsi/parser.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-package main
+package zephyr_dtsi
 
 import (
 	"encoding/csv"
diff --git a/zephyr_dtsi/parser_test.go b/zephyr_dtsi/parser_test.go
index 3468617..ea0a7fd 100644
--- a/zephyr_dtsi/parser_test.go
+++ b/zephyr_dtsi/parser_test.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-package main
+package zephyr_dtsi
 
 import (
 	"strings"
diff --git a/zephyr_dtsi/run.go b/zephyr_dtsi/run.go
new file mode 100644
index 0000000..a731bf9
--- /dev/null
+++ b/zephyr_dtsi/run.go
@@ -0,0 +1,104 @@
+// Copyright 2026 The ChromiumOS Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package zephyr_dtsi
+
+import (
+	"errors"
+	"flag"
+	"fmt"
+	"io"
+	"os"
+	"path/filepath"
+)
+
+// Run executes the EC firmware pinmux DTSI generation logic.
+func Run(args []string) error {
+	fs := flag.NewFlagSet("zephyr_dtsi", flag.ContinueOnError)
+	chipFlag := fs.String("chip", "", "Supported EC chip name or alias (e.g. npcx99, it82002)")
+	csvFlag := fs.String("csv", "", "Path to the local EC pinout CSV file")
+	pinColFlag := fs.String("pin-col", "", "Optional: Column header containing pin numbers")
+	chipDataDirFlag := fs.String("chip-data-dir", "", "Optional: Path to directory containing custom chip JSON data files")
+	outputFlag := fs.String("output", "", "Path to the output DTSI file (defaults to stdout)")
+
+	if err := fs.Parse(args); err != nil {
+		if errors.Is(err, flag.ErrHelp) {
+			return nil
+		}
+		return err
+	}
+	if len(fs.Args()) > 0 {
+		return fmt.Errorf("unexpected arguments: %v", fs.Args())
+	}
+
+	if *chipDataDirFlag != "" {
+		if err := LoadExternalChipDir(*chipDataDirFlag); err != nil {
+			return fmt.Errorf("loading chip data dir %s: %w", *chipDataDirFlag, err)
+		}
+	}
+
+	if *chipFlag == "" {
+		return fmt.Errorf("--chip is required (available: %s)", GetAvailableChips())
+	}
+	if *csvFlag == "" {
+		return fmt.Errorf("--csv is required")
+	}
+
+	chip, ok := GetChip(*chipFlag)
+	if !ok {
+		return fmt.Errorf("unsupported chip %q (available: %s)", *chipFlag, GetAvailableChips())
+	}
+
+	csvFile, err := os.Open(*csvFlag)
+	if err != nil {
+		return fmt.Errorf("opening CSV file: %w", err)
+	}
+	defer csvFile.Close()
+
+	configs, err := ParseCSV(csvFile, *pinColFlag)
+	if err != nil {
+		return fmt.Errorf("parsing CSV: %w", err)
+	}
+
+	var out io.Writer = os.Stdout
+	var tmpFile *os.File
+	var tmpPath string
+	if *outputFlag != "" {
+		dir := filepath.Dir(*outputFlag)
+		if err := os.MkdirAll(dir, 0755); err != nil {
+			return fmt.Errorf("creating output directory: %w", err)
+		}
+		tmp, err := os.CreateTemp(dir, ".tmp_dtsi_*")
+		if err != nil {
+			return fmt.Errorf("creating temporary file: %w", err)
+		}
+		tmpFile = tmp
+		tmpPath = tmp.Name()
+		out = tmp
+	}
+	defer func() {
+		if tmpFile != nil {
+			_ = tmpFile.Close()
+			if tmpPath != "" {
+				_ = os.Remove(tmpPath)
+			}
+		}
+	}()
+
+	if err := GenerateDTSI(out, configs, chip); err != nil {
+		return fmt.Errorf("generating DTSI: %w", err)
+	}
+
+	if tmpFile != nil {
+		if err := tmpFile.Close(); err != nil {
+			return fmt.Errorf("closing output file: %w", err)
+		}
+		if err := os.Rename(tmpPath, *outputFlag); err != nil {
+			return fmt.Errorf("finalizing output file: %w", err)
+		}
+		tmpFile = nil
+	}
+
+	return nil
+}
diff --git a/zephyr_dtsi/run_test.go b/zephyr_dtsi/run_test.go
new file mode 100644
index 0000000..c363cba
--- /dev/null
+++ b/zephyr_dtsi/run_test.go
@@ -0,0 +1,52 @@
+// Copyright 2026 The ChromiumOS Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package zephyr_dtsi
+
+import (
+	"os"
+	"path/filepath"
+	"strings"
+	"testing"
+)
+
+func TestDtsiTool_RunFlagValidation(t *testing.T) {
+	if err := Run([]string{"-h"}); err != nil {
+		t.Errorf("Run('-h') unexpected error: %v", err)
+	}
+	if err := Run([]string{"--help"}); err != nil {
+		t.Errorf("Run(--help) expected nil, got %v", err)
+	}
+	if err := Run([]string{}); err == nil {
+		t.Errorf("expected error with no flags")
+	}
+	if err := Run([]string{"--chip=npcx99"}); err == nil {
+		t.Errorf("expected error without --csv")
+	}
+	if err := Run([]string{"--chip=invalid", "--csv=test/ExampleEcPinout.csv"}); err == nil {
+		t.Errorf("expected error with invalid chip")
+	}
+}
+
+func TestDtsiTool_RunSuccess(t *testing.T) {
+	out := filepath.Join(t.TempDir(), "out.dtsi")
+	err := Run([]string{
+		"--chip=npcx99",
+		"--pin-col=NPCX99 (NPCX)",
+		"--csv=test/ExampleEcPinout.csv",
+		"--output=" + out,
+	})
+	if err != nil {
+		t.Fatalf("Run failed: %v", err)
+	}
+
+	data, err := os.ReadFile(out)
+	if err != nil {
+		t.Fatalf("failed to read generated file: %v", err)
+	}
+	content := string(data)
+	if !strings.Contains(content, "named-gpios") {
+		t.Errorf("expected generated DTSI to contain named-gpios")
+	}
+}
diff --git a/zephyr_dtsi/test/README.md b/zephyr_dtsi/test/README.md
index f49146b..2cdcf3f 100644
--- a/zephyr_dtsi/test/README.md
+++ b/zephyr_dtsi/test/README.md
@@ -1,6 +1,6 @@
-# `ec_fw` Test & Example Directory
+# `zephyr_dtsi` Test & Example Directory
 
-This directory contains reference example files for validating and testing the `ec_fw` EC Devicetree generator.
+This directory contains reference example files for validating and testing the `zephyr_dtsi` EC Devicetree generator.
 
 ---
 
@@ -18,10 +18,10 @@
 Run the following command from the repository root:
 
 ```bash
-go run ./ec_fw --chip=npcx99 \
-               --csv=ec_fw/test/ExampleEcPinout.csv \
+./firmweaver dtsi --chip=npcx99 \
+               --csv=zephyr_dtsi/test/ExampleEcPinout.csv \
                --pin-col="NPCX99 (NPCX)" \
-               --output=ec_fw/test/generated.dtsi
+               --output=zephyr_dtsi/test/generated.dtsi
 ```
 
 ### 2. Generate Reference Devicetree (`IT82002` Variant - ITE IT82002)
@@ -29,8 +29,8 @@
 To generate for an ITE-based variant from the same spreadsheet:
 
 ```bash
-go run ./ec_fw --chip=it82002 \
-               --csv=ec_fw/test/ExampleEcPinout.csv \
+./firmweaver dtsi --chip=it82002 \
+               --csv=zephyr_dtsi/test/ExampleEcPinout.csv \
                --pin-col="IT82002 (ITE)" \
-               --output=ec_fw/test/generated_ite.dtsi
+               --output=zephyr_dtsi/test/generated_ite.dtsi
 ```