Skip to main content

add-cli-command

Add a new subcommand to the Memory Snapshot Data Tool CLI (the .NET 10 `MemorySnapshotDataTools` exe), wiring it through CliOptions → CommandLineBuilder → Program → a Core runner, then documenting it in README.md. Use when asked to add, create, or expose a new CLI command/subcommand (alongside export, batch-export, report, multi-report, validate, summary, upgrade), or a new flag/argument on an existing one.

Source facts

Repository
Unity-Technologies/MemorySnapshotDataTools
Last source activity
June 5, 2026 at 18:52
Detected SKILL.md language
English
Stars
11
Forks
1

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
add-cli-command
description
Add a new subcommand to the Memory Snapshot Data Tool CLI (the .NET 10 `MemorySnapshotDataTools` exe), wiring it through CliOptions → CommandLineBuilder → Program → a Core runner, then documenting it in README.md. Use when asked to add, create, or expose a new CLI command/subcommand (alongside export, batch-export, report, multi-report, validate, summary, upgrade), or a new flag/argument on an existing one.
# Add a new CLI command The CLI is built with `System.CommandLine`. A subcommand is wired through **three files in `Cli/`** plus a **Core runner** that holds the logic, and is then **documented in `README.md`**. Match the existing commands (`export`, `batch-export`, `report`, `multi-report`, `validate`, `summary`, `upgrade`) — don't invent a new shape. **Paths below are relative to the repo root** (the directory containing `MemorySnapshotDataTools.sln`). ## When to use - Adding a brand-new subcommand (e.g. `diff`, `prune`, `export-csv`). - Adding a new argument or `--option` to an existing subcommand. - Exposing an existing Core capability through the CLI. For SQL-touching logic inside the new command, also follow [`CLAUDE.md`](../../../CLAUDE.md) and [`docs/sql-safety.md`](../../../docs/sql-safety.md) — SQL safety is a first-class rule here. ## The files you touch | File | What you add | |------|--------------| | [`Cli/CliOptions.cs`](../../../Cli/CliOptions.cs) | A `CommandKind` enum value + any new option/arg properties on `CliOptions`. | | [`Cli/CommandLineBuilder.cs`](../../../Cli/CommandLineBuilder.cs) | The `Command`, its `Argument<>`/`Option<>`s, the `SetAction` handler, `root.Add(...)`, and a new `Func<CliOptions,int>` parameter on `Build`. | | [`Cli/Program.cs`](../../../Cli/Program.cs) | A `RunXxx(CliOptions)` handler that calls the Core runner, plus threading it through the `Build(...)` call in `Main`. | | `Core/...` (a runner) | The actual logic — a `XxxRunner` + a `XxxRunOptions`, mirroring `Core/Report/SummaryReportRunner.cs`, `Core/Export/ExportRunner.cs`, etc. | | [`Tests/`](../../../Tests) | Tests for the **Core runner / calculator** (there are no `CommandLineBuilder` tests; test the logic, not the parser). | | [`README.md`](../../../README.md) | **Required** — a usage subsection + example for the new command. See [Document in README.md](#document-in-readmemd). | ## Step by step ### 1. `Cli/CliOptions.cs` - Add a value to the `CommandKind` enum. - Add a property to `CliOptions` for each new argument/option. Reuse existing ones where they fit (`Verbose`, `ReportDbPath`, `Destination`, …) before adding new fields. ### 2. `Cli/CommandLineBuilder.cs` Inside `Build(...)`, following the existing blocks: ```csharp // ---- mycommand ---- var myCmd = new Command("mycommand", "One-line description shown in --help."); var inputArg = new Argument<string>("input") { Description = "Path to ...", Arity = ArgumentArity.ExactlyOne, }; myCmd.Add(inputArg); var someOpt = new Option<string>("--mode") { Description = "...: a, b, or c.", DefaultValueFactory = _ => "a", }; someOpt.AcceptOnlyFromAmong("a", "b", "c"); // validate enum-like options at parse time myCmd.Add(someOpt); myCmd.SetAction((ParseResult parseResult) => { var inputPath = ExpandPath(parseResult.GetValue(inputArg)!); // ALWAYS ExpandPath path args if (!File.Exists(inputPath)) // validate existence, return 1 { Console.Error.WriteLine($"Input file not found: {inputPath}"); return 1; } var options = new CliOptions { Command = CommandKind.MyCommand, // ... map parsed values onto CliOptions ... }; return runMyCommand(options); }); ``` Then: - Register it: `root.Add(myCmd);` near the bottom of `Build`. - Add a `Func<CliOptions, int> runMyCommand` parameter to the `Build(...)` signature (the params are positional — keep the order consistent with `Program.Main`). ### 3. `Cli/Program.cs` - Add a `RunMyCommand(CliOptions options)` static handler that delegates to your Core runner. - If the command **reads an exported database**, call `SchemaGate.Check(path)` first (see `RunReport`/`RunSummary`). - If it does cancellable work, use `CreateCancellationSource()` and catch `OperationCanceledException` → return `2` (see `RunExport`). - Add `RunMyCommand` to the `CommandLineBuilder.Build(...)` call in `Main` in the matching position. ### 4. Core runner (the logic) Put real work in `Core/...`, not in `Cli/`. Mirror an existing runner (`SummaryReportRunner`, `ExportRunner`): a `XxxRunOptions` record + a `static int Run(...)` that returns an exit code and reports via `IProgressReporter`. If it builds SQL, parameterize values and validate identifiers per [`docs/sql-safety.md`](../../../docs/sql-safety.md); open the DB **read-only** if it only reads (`Mode=ReadOnly` / `ACCESS_MODE=READ_ONLY`). ### 5. Tests Add tests under [`Tests/`](../../../Tests) targeting the Core runner / calculator (e.g. `BatchExportRunnerTests.cs`, `SummaryMetricsCalculatorTests.cs`). Helpers you assert on must be **`public`** — `InternalsVisibleTo` is a no-op here, so Core internals are not visible to Tests. ### 6. Document in README.md **This step is required — a new command is not done until the README documents it.** In [`README.md`](../../../README.md): - Add a `### <verb the command does>` subsection under **How to use**, after the existing command subsections, with the invocation form and a worked example, matching the style of the `export` and `report` sections: ````markdown ### <What the command does> ```bash dotnet run --project Cli/MemorySnapshotDataTools.Cli.csproj -- mycommand <args> [options] ``` - **`--option`:** what it does (default: …). **Example:** ```bash dotnet run --project Cli/MemorySnapshotDataTools.Cli.csproj -- mycommand ./in.duckdb --mode b ``` ```` - If the command changes the headline behavior, add a bullet to the **What it does** list at the top, and update **Output**/**Schema** if it writes new tables/files. - Consider also updating, when relevant: the **Direct CLI invocation** list in [`run-memory-snapshot-data-tool/SKILL.md`](../run-memory-snapshot-data-tool/SKILL.md), and `docs/intro.md` / `docs/runbook.md`. ### 7. Build, test, and verify it runs ```bash dotnet build MemorySnapshotDataTools.sln dotnet test MemorySnapshotDataTools.sln dotnet run --project Cli/MemorySnapshotDataTools.Cli.csproj -- --help # command listed? dotnet run --project Cli/MemorySnapshotDataTools.Cli.csproj -- mycommand --help # args/options correct? ``` To exercise the full pipeline (build → run → screenshot), use the `run-memory-snapshot-data-tool` skill's driver. ## Conventions & gotchas - **Always `ExpandPath(...)` path arguments/options** before use — it expands `~`, env vars, and resolves to a full path. Every existing handler does this. - **Validate inputs in `SetAction` and return `1`** for bad args / missing files, before constructing `CliOptions`. - **Exit codes are meaningful and asserted on:** `0` success, `1` bad args / file not found, `2` cancelled (Ctrl-C), `3` validation/error. Keep yours consistent. - **Enum-like string options** → `option.AcceptOnlyFromAmong("a","b","c")` so bad values are rejected at parse time (see `--validate`, `--destination`). - **`Build`'s handlers are positional `Func<CliOptions,int>` parameters.** Add yours in the same position in both the `Build` signature and the `Main` call site, or the wrong handler runs. - **Reading an exported DB?** Call `SchemaGate.Check(path)` first so a stale/newer schema fails with a clear message instead of a confusing query error. - **SQL safety is non-negotiable** — never interpolate external values into SQL; bind parameters and validate identifiers (`CLAUDE.md`, `docs/sql-safety.md`). ## Checklist - [ ] `CommandKind` value + `CliOptions` properties added (`Cli/CliOptions.cs`). - [ ] Command, args/options, `SetAction`, `root.Add`, and new `Build` param added (`Cli/CommandLineBuilder.cs`). - [ ] `RunXxx` handler added and threaded through `Main` (`Cli/Program.cs`). - [ ] Logic lives in a Core `XxxRunner` (+ `XxxRunOptions`); SQL is parameterized / read-only. - [ ] Tests added for the Core logic (public helpers). - [ ] **`README.md` documents the command** (usage + example), other docs/skill updated if relevant. - [ ] `dotnet build` + `dotnet test` pass; `-- --help` and `-- mycommand --help` look right.
View on GitHub