Skip to content

feat(cli): subcommands + stdin/stdout filter mode (ergonomics 2-3/3)#36

Merged
smorin merged 6 commits into
mainfrom
feat/cli-subcommands
May 30, 2026
Merged

feat(cli): subcommands + stdin/stdout filter mode (ergonomics 2-3/3)#36
smorin merged 6 commits into
mainfrom
feat/cli-subcommands

Conversation

@smorin

@smorin smorin commented May 30, 2026

Copy link
Copy Markdown
Owner

Summary

Completes the CLI ergonomics refactor (P10) — options 2 and 3 of 3, stacked on the just-merged declarative-constraints PR (#35).

Option 1 — additive subcommands

Adds toggle / scan / check / list / insert / remove, each exposing only the flags relevant to that operation. Additive and zero-drift by construction: the legacy flat-flag interface is untouched; each subcommand translates itself to the equivalent legacy argv (Commands::to_legacy_argv) and is re-parsed through the same build_command() path, so clap stays the single source of truth for defaults, validation, and binary-name aliasing. The legacy form emits a one-line, TTY-only deprecation nudge (silent under --json, pipes, and meta/no-op invocations).

Option 3 — stdin/stdout filter mode

A single stream-to-stdout transform for the writer ops (toggle/insert/remove), never modifying a file:

  • stdin → stdout: a - path, --stdin, or --stdout with no path.
  • file → stdout: toggle file.py --stdout -S feat reads the file (real extension → real comment style), transforms, and prints to stdout, leaving disk untouched — the prettier/clang-format model.

Filter mode rejects flags/inputs that collide with single-stream stdout output (--json, --atomic, --backup, --dry-run, --interactive, -R, multiple files, directories, mixing - with a file path) and the read-only operations.

Tests

+88 net tests (303 total green), built on a parity discipline:

  • Subcommand ≡ flat-flag parity for every operation, including a defaulted --remove-mode case (guards default drift) and an --atomic -R multi-file case through the bridge.
  • Filter mode: stdin≡file byte parity, file→stdout parity + file-unmodified, real-extension comment style (.js//), no-op byte identity (trailing newline both ways), and the full rejection set.
  • --help / --man / --completions render subcommands under the aliased bin name.

Notes

  • cargo test, cargo clippy --all-targets -- -D warnings, and cargo fmt --check all clean.
  • No FFI changes; the committed C header is untouched.
  • Ships in the next release-please batch (v0.5.0).

Refs P10.

smorin added 5 commits May 30, 2026 14:35
Add `toggle`/`scan`/`check`/`list`/`insert`/`remove` subcommands, each
exposing only the flags that apply to that operation. The layer is purely
additive: the legacy flat-flag interface is unchanged, and each subcommand
translates itself to the equivalent legacy argv (`Commands::to_legacy_argv`)
which is re-parsed through the same `build_command()` path. clap therefore
stays the single source of truth for defaults, validation, and binary-name
aliasing, making subcommand/flat-flag drift structurally impossible.

The legacy flat-flag form emits a one-line, TTY-only deprecation nudge
(suppressed under --json, non-TTY stderr, and meta/no-op invocations) so
scripts and pipes are unaffected.

Adds 15 parity tests asserting each subcommand is behavior-identical to its
flat-flag equivalent, including a defaulted `--remove-mode` case that guards
against silent default drift, plus scoping and help/man-render checks.

Refs P10-T02..T04, P10-TS01.
Add a single stdin->stdout filter mode with three spellings — a `-` path,
`--stdin`, or `--stdout` — for the writer operations (toggle/insert/remove).
Input is read from stdin and the transformed result is written to stdout; the
file is never touched. Per the design, this is one mode (stdin in, stdout out),
not an input x output matrix: a real file path combined with --stdin/--stdout
is rejected.

Comment style for the pathless stream resolves from --comment-style if given,
else defaults to Python `#` (a synthetic `<stdin>.py` path); pipe other
languages with --comment-style.

Filter mode rejects flags that collide with stdout output (--json, --atomic,
--backup, --interactive, --dry-run, -R) and the read-only operations. Detection
runs after the meta/recover/journal short-circuits and before the file-oriented
arity checks, so a stdin stream is never rejected as a missing file path.

Adds 14 tests: stdin-equals-file byte parity for each writer op, no-op
byte-identity preserving exact trailing-newline handling both ways, spelling
equivalence, and the full rejection set. Insert's subcommand path becomes
optional so `insert --stdin` needs no positional. New io helpers
`read_stdin_encoded` / `write_stdout_encoded` mirror the file-encoded variants.

Refs P10-T05, P10-TS02. Completes P10.
Previously `--stdout` was a pure alias of stdin filter mode. It now also accepts
a real file path: `toggle file.py --stdout -S feat` reads that file, applies the
transform, and writes the result to stdout without modifying the file — the
prettier/clang-format model, useful for editor integration and previewing.

Because a real file has a real extension, its comment style resolves normally
(e.g. a `.js` file uses `//`), unlike piped stdin which falls back to the
synthetic `<stdin>.py` Python default. Filter mode remains a single stream to
stdout: mixing `-`/`--stdin` with a file path, multiple files, or a directory
is rejected.

Adds 7 tests: file→stdout byte parity with the in-place result, file-unmodified,
real-extension comment style (.js → //), and the new rejection cases. Updates
README and --stdout help text.

Refs P10-T05.
Copilot AI review requested due to automatic review settings May 30, 2026 21:35
…slash)

Run both forms against the same file instead of normalizing two temp paths
textually; JSON escapes \ on Windows so the raw-path replace never matched.
All callers are non-mutating (scan/check/list/--dry-run), so sharing one file
is safe and makes paths byte-identical.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Completes the CLI ergonomics refactor by adding an additive subcommand front-end and a stdin→stdout “filter mode” for writer operations, while keeping the legacy flat-flag pipeline as the execution path.

Changes:

  • Add Commands subcommands that translate to legacy argv and re-parse through the canonical clap build_command() path, plus a TTY-only legacy deprecation nudge.
  • Implement stdin/stdout filter mode (- / --stdin / --stdout) for toggle/insert/remove, backed by new lib I/O helpers.
  • Add dedicated integration tests for subcommand≡legacy parity and filter-mode behavior; update docs/project notes accordingly.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
README.md Documents new subcommands and filter mode usage.
PROJECTS.md Records completion of Project P10 (CLI ergonomics refactor).
crates/togl-lib/src/io.rs Adds stdin read + stdout write helpers with encoding support.
crates/togl-cli/src/cli.rs Introduces subcommand CLI surface (Commands, GlobalArgs, FilterArgs) and legacy argv translation.
crates/togl-cli/src/main.rs Adds subcommand bridge, legacy deprecation notice, and filter-mode execution path.
crates/togl-cli/tests/subcommands.rs Parity tests to pin subcommand behavior to legacy flat-flag behavior.
crates/togl-cli/tests/filter.rs Tests for stdin/stdout filter mode, parity, and rejection constraints.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread README.md

| Subcommand | Flat-flag equivalent |
|---|---|
| `toggle <paths> -S id` | `toggle <paths> -S id` |
Comment thread README.md
Comment on lines +48 to +49
Run `toggle <subcommand> --help` to see its scoped flags. The flat-flag form
still works and is supported, but is deprecated in favor of the subcommands.
Comment on lines +215 to +217
/// Flags shared by every subcommand. Flattened into each variant so the legacy
/// definitions on `Cli` stay untouched (additive, not a restructure).
#[derive(clap::Args, Debug)]
Comment on lines +441 to +442
/// Single file to modify. Omit (with --stdin) or pass `-` to read stdin.
path: Option<PathBuf>,
@smorin smorin merged commit c7309b8 into main May 30, 2026
16 checks passed
@smorin smorin deleted the feat/cli-subcommands branch May 30, 2026 21:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants