# How to Add New Commands to OfficeCLI: Extending the CommandBuilder Pattern

> Extend OfficeCLI by adding new custom commands. Learn how to leverage the CommandBuilder pattern and register your commands directly in Program.cs for powerful customization.

- Repository: [OfficeAI/OfficeCLI](https://github.com/iofficeai/OfficeCLI)
- Tags: how-to-guide
- Published: 2026-07-26

---

**To add a new command to OfficeCLI, create a `static` method in the `partial class CommandBuilder` that returns a configured `System.CommandLine.Command` object, then register it in the `RootCommand` assembly located in [`Program.cs`](https://github.com/iOfficeAI/OfficeCLI/blob/main/Program.cs).**

OfficeCLI is a .NET-based command-line interface for Office documents that leverages the **System.CommandLine** library with a modular **CommandBuilder** pattern. If you need to extend OfficeCLI with custom verbs, understanding the `CommandBuilder` partial class architecture in the `src/officecli` directory is essential. This guide walks through the exact implementation pattern used in the iOfficeAI/OfficeCLI repository, providing runnable code examples that mirror the built-in `save`, `refresh`, and `plugins` commands.

## Understanding the CommandBuilder Architecture

OfficeCLI organizes its command-line interface through a `partial static class CommandBuilder` located in the `OfficeCli` namespace. Rather than defining all verbs in a single file, the architecture splits each command into its own partial class file, such as [`CommandBuilder.Save.cs`](https://github.com/iOfficeAI/OfficeCLI/blob/main/CommandBuilder.Save.cs), [`CommandBuilder.Refresh.cs`](https://github.com/iOfficeAI/OfficeCLI/blob/main/CommandBuilder.Refresh.cs), and [`CommandBuilder.Plugins.cs`](https://github.com/iOfficeAI/OfficeCLI/blob/main/CommandBuilder.Plugins.cs).

### The Partial Class Pattern

Each command file implements a **private static method** following the naming convention `Build{Command}Command`. These methods return a `Command` object configured with arguments, options, and action handlers. This separation ensures that adding new commands never requires modifying existing command logic—you simply create a new file and register the method in [`Program.cs`](https://github.com/iOfficeAI/OfficeCLI/blob/main/Program.cs).

### Command Structure Standards

Every command builder method follows a consistent template:

1. Define **arguments** using `Argument<T>` for positional parameters
2. Define **options** using `Option<T>` for flags like `--json`
3. Compose a `Command` instance with a description
4. Call **`SetAction`** to register the handler, typically wrapped with the `SafeRun` helper for consistent error handling
5. Return the constructed `Command`

## Step-by-Step: Adding a New Command to OfficeCLI

### Step 1 - Create the Command Builder Method

Create a new file in `src/officecli/` following the existing naming convention. For example, to add a `hello` command, create [`CommandBuilder.Hello.cs`](https://github.com/iOfficeAI/OfficeCLI/blob/main/CommandBuilder.Hello.cs):

```csharp
// src/officecli/CommandBuilder.Hello.cs
// -------------------------------------------------
using System.CommandLine;

namespace OfficeCli;

static partial class CommandBuilder
{
    private static Command BuildHelloCommand(Option<bool> jsonOption)
    {
        var nameArg = new Argument<string>("name")
        {
            Description = "Name to greet"
        };

        var helloCmd = new Command("hello", "Print a friendly greeting.");
        helloCmd.Add(nameArg);
        helloCmd.Add(jsonOption);

        helloCmd.SetAction(result => {
            var json = result.GetValue(jsonOption);
            var name = result.GetValue(nameArg)!;
            var msg = $"Hello, {name}!";

            if (json)
                Console.WriteLine(OutputFormatter.WrapEnvelopeText(msg));
            else
                Console.WriteLine(msg);

            return 0;
        });

        return helloCmd;
    }
}

```

### Step 2 - Register in the Root Command

After creating the builder method, expose the command by adding it to the `RootCommand` in [`Program.cs`](https://github.com/iOfficeAI/OfficeCLI/blob/main/Program.cs):

```csharp
// src/officecli/Program.cs (excerpt)
// -------------------------------------------------
var root = new RootCommand("OfficeCLI – command‑line interface for Office documents");

// Existing commands
root.AddCommand(CommandBuilder.BuildSaveCommand(jsonOption));
root.AddCommand(CommandBuilder.BuildRefreshCommand(jsonOption));
// ...

// Register the new command
root.AddCommand(CommandBuilder.BuildHelloCommand(jsonOption));

return await root.InvokeAsync(args);

```

## Consistent Error Handling and JSON Output

All built-in commands in OfficeCLI support a `--json` flag for machine-readable output and use the `SafeRun` helper for uniform error handling. According to the source code in [`CommandBuilder.Save.cs`](https://github.com/iOfficeAI/OfficeCLI/blob/main/CommandBuilder.Save.cs), you should:

- Accept the shared `Option<bool> jsonOption` parameter in your builder method
- Add the option to your command via `helloCmd.Add(jsonOption)`
- Check the flag value inside the `SetAction` delegate
- Use `OutputFormatter.WrapEnvelopeText()` when JSON mode is enabled
- Wrap your logic with `SafeRun` (omitted in the example above for brevity) to ensure exceptions are caught and formatted consistently with other commands

## Testing Your New Command with SDK Smoke Tests

After implementing a new command, validate it through the SDK smoke test harness. The iOfficeAI/OfficeCLI repository maintains test suites in:

- [`sdk/node/smoke.js`](https://github.com/iOfficeAI/OfficeCLI/blob/main/sdk/node/smoke.js) (Node.js SDK tests)
- [`sdk/python/smoke.py`](https://github.com/iOfficeAI/OfficeCLI/blob/main/sdk/python/smoke.py) (Python SDK tests)

Add a corresponding test entry that invokes your new command and verifies the exit code and output format. This ensures CI pipelines catch regressions and that the command works correctly through the SDK wrappers.

## Summary

- Create a new partial class file following the `CommandBuilder.{Command}.cs` naming convention in `src/officecli/`
- Implement a `private static Command Build{Command}Command(Option<bool> jsonOption)` method that constructs and returns a `Command` object
- Configure **arguments** using `Argument<T>` and **options** using `Option<T>`
- Call `SetAction` to register the handler, referencing the `jsonOption` value for consistent output formatting
- Register the new command in [`Program.cs`](https://github.com/iOfficeAI/OfficeCLI/blob/main/Program.cs) via `root.AddCommand(CommandBuilder.Build{Command}Command(jsonOption))`
- Add smoke tests in [`sdk/node/smoke.js`](https://github.com/iOfficeAI/OfficeCLI/blob/main/sdk/node/smoke.js) or [`sdk/python/smoke.py`](https://github.com/iOfficeAI/OfficeCLI/blob/main/sdk/python/smoke.py) to verify CLI behavior and exit codes

## Frequently Asked Questions

### What is the CommandBuilder pattern in OfficeCLI?

The **CommandBuilder pattern** is a modular architecture using a `partial static class CommandBuilder` where each command verb is implemented as a separate static method returning a `System.CommandLine.Command` object. This pattern, located in the `src/officecli` directory, separates concerns by placing each verb's logic in its own file (e.g., [`CommandBuilder.Save.cs`](https://github.com/iOfficeAI/OfficeCLI/blob/main/CommandBuilder.Save.cs)) while maintaining a consistent interface for argument parsing and error handling across the entire CLI.

### Do I need to modify existing files to add a new command?

No. You create a new file for your command logic (e.g., [`CommandBuilder.Hello.cs`](https://github.com/iOfficeAI/OfficeCLI/blob/main/CommandBuilder.Hello.cs)) since `CommandBuilder` is declared as a `partial` class. However, you must modify [`Program.cs`](https://github.com/iOfficeAI/OfficeCLI/blob/main/Program.cs) to register the new command by calling your builder method and adding the returned `Command` to the `RootCommand` instance using `root.AddCommand()`.

### How does OfficeCLI handle JSON output for new commands?

All commands share a common `Option<bool> jsonOption` that enables structured output. According to the source code in files like [`src/officecli/CommandBuilder.Save.cs`](https://github.com/iOfficeAI/OfficeCLI/blob/main/src/officecli/CommandBuilder.Save.cs), you should add this option to your command and check its value inside the `SetAction` handler. When enabled, wrap output using `OutputFormatter.WrapEnvelopeText()` to ensure consistent JSON formatting across the CLI.

### Where should I add tests for custom OfficeCLI commands?

After implementing a new command, add corresponding smoke tests in the SDK test harnesses located at [`sdk/node/smoke.js`](https://github.com/iOfficeAI/OfficeCLI/blob/main/sdk/node/smoke.js) and [`sdk/python/smoke.py`](https://github.com/iOfficeAI/OfficeCLI/blob/main/sdk/python/smoke.py). These tests validate that the command responds correctly to invocation and returns the expected exit codes, ensuring CI/CD pipelines catch regressions and that the command integrates properly with the Node.js and Python SDKs.