Skip to content

feat!: consolidate cot cli commands into one - #587

Open
ElijahAhianyo wants to merge 18 commits into
masterfrom
elijah/cot-proxy-cmd
Open

feat!: consolidate cot cli commands into one#587
ElijahAhianyo wants to merge 18 commits into
masterfrom
elijah/cot-proxy-cmd

Conversation

@ElijahAhianyo

@ElijahAhianyo ElijahAhianyo commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Background

cot currently exposes CLI commands through two separate entry points:

  • cot-cli crate (cot <command>): handles project scaffolding, migration listing, migration generation, and shell completions.
  • Compiled binary (invoked directly): exposes runtime commands such as check, running migrations (which is also triggered implicitly at startup), and any custom user-defined task commands.

In addition, running and building a cot app relies on Cargo tooling. In summary, there are three ways to invoke CLI commands today:

  1. cot <command> via the cot-cli crate
  2. Invoking the compiled binary directly for commands not available in cot-cli
  3. Using Cargo to build and run apps

This PR focuses on unifying 1 and 2 for ergonomics: cot now acts as the single entry point for all commands, proxying any unrecognized command to the compiled binary if it exists there. Option 3 (Cargo invocation) is out of scope for this PR and can be revisited in a follow-up if proxying those commands makes sense.

Approach

When cot receives a command it does not recognize, it resolves the target binary (target/debug by default, or target/release if --release is passed), queries it for its available commands via a metadata flag, and either forwards the command along with all provided arguments or returns an error if the command is not found in the binary either.

Metadata

To support proxying, the cot crate exposes a --metadata flag. At runtime, the binary uses reflection to enumerate all registered CLI commands and prints them as JSON to stdout. This serves two purposes: it tells cot-cli whether a given command should be forwarded, and it provides the information needed to render accurate help text.

Example:

$ ./target/debug/forms --metadata
{
  "binary_name": "forms",
  "commands": [
    {
      "name": "check",
      "about": "Verifies the configuration, including connections to the database and other services",
      "aliases": [],
      "subcommands": []
    },
    {
      "name": "collect-static",
      "about": "Collects all static files into a static directory",
      "aliases": [],
      "subcommands": []
    }
  ]
}

Caching

Querying the binary on every invocation would be wasteful, so the metadata response is cached in .cot/command-cache.json. The cache stores the binary's modified time (mtime) alongside the metadata and is invalidated automatically whenever the binary is rebuilt.

Workspaces

When working inside a Cargo workspace, a --package (-p) flag must be provided to specify which package's binary should be targeted.

Type of change

  • Bug fix
  • New feature
  • Documentation
  • Refactor / cleanup
  • Performance improvement
  • Other (describe above)

@github-actions github-actions Bot added A-deps Area: Dependencies C-cli Crate: cot-cli (issues and Pull Requests related to Cot CLI) C-lib Crate: cot (main library crate) labels Jun 10, 2026
@ElijahAhianyo ElijahAhianyo changed the title consolidate cot cli commands into one feat!: consolidate cot cli commands into one Jun 10, 2026
@github-actions

github-actions Bot commented Jun 10, 2026

Copy link
Copy Markdown

🐰 Bencher Report

Projectcot
Branchelijah/cot-proxy-cmd
Testbedgithub-ubuntu-latest

🚨 1 Alert

BenchmarkMeasure
Units
ViewBenchmark Result
(Result Δ%)
Upper Boundary
(Limit %)
empty_router/empty_routerLatency
milliseconds (ms)
📈 plot
🚷 threshold
🚨 alert (🔔)
13.67 ms
(+88.48%)Baseline: 7.25 ms
12.55 ms
(108.92%)

Click to view all benchmark results
BenchmarkLatencyBenchmark Result
milliseconds (ms)
(Result Δ%)
Upper Boundary
milliseconds (ms)
(Limit %)
empty_router/empty_router📈 view plot
🚷 view threshold
🚨 view alert (🔔)
13.67 ms
(+88.48%)Baseline: 7.25 ms
12.55 ms
(108.92%)

json_api/json_api📈 view plot
🚷 view threshold
1.13 ms
(+4.61%)Baseline: 1.08 ms
1.37 ms
(82.69%)
nested_routers/nested_routers📈 view plot
🚷 view threshold
1.05 ms
(+4.51%)Baseline: 1.01 ms
1.24 ms
(84.58%)
single_root_route/single_root_route📈 view plot
🚷 view threshold
1.02 ms
(+4.72%)Baseline: 0.97 ms
1.21 ms
(84.17%)
single_root_route_burst/single_root_route_burst📈 view plot
🚷 view threshold
20.14 ms
(+15.91%)Baseline: 17.37 ms
21.69 ms
(92.85%)
🐰 View full continuous benchmarking report in Bencher

@ElijahAhianyo
ElijahAhianyo marked this pull request as ready for review June 23, 2026 16:41
@codecov

codecov Bot commented Jun 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.53237% with 38 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
cot-cli/src/project.rs 96.36% 5 Missing and 10 partials ⚠️
cot-cli/src/handlers.rs 92.74% 6 Missing and 3 partials ⚠️
cot-cli/src/main.rs 90.16% 4 Missing and 2 partials ⚠️
cot/src/project.rs 0.00% 5 Missing ⚠️
cot/src/cli.rs 0.00% 3 Missing ⚠️
Flag Coverage Δ
rust 90.42% <94.53%> (+0.16%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
cot-cli/src/args.rs 100.00% <100.00%> (ø)
cot-cli/src/utils.rs 95.41% <100.00%> (+1.93%) ⬆️
cot/src/metadata.rs 100.00% <100.00%> (ø)
cot/src/cli.rs 82.14% <0.00%> (-1.12%) ⬇️
cot/src/project.rs 88.21% <0.00%> (-0.56%) ⬇️
cot-cli/src/main.rs 92.40% <90.16%> (-7.60%) ⬇️
cot-cli/src/handlers.rs 94.33% <92.74%> (-1.61%) ⬇️
cot-cli/src/project.rs 96.36% <96.36%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@ElijahAhianyo
ElijahAhianyo requested review from m4tx and seqre June 25, 2026 11:53
Comment thread cot-cli/src/handlers.rs
Comment on lines +134 to +144
#[cfg(unix)]
{
let err = std::process::Command::new(&proj.path).args(args).exec();
anyhow::bail!("Failed to exec {}: {err}", proj.path.display());
}

#[cfg(not(unix))]
{
let status = std::process::Command::new(&proj.path).args(args).status()?;
std::process::exit(status.code().unwrap_or(1));
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Where's the difference between Unix-like and non-Unix-like platforms coming from? Why do we need separate code paths here?

Comment thread cot-cli/src/project.rs

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do we need all this machinery here? Any chance that running cargo run would suffice instead?

Comment thread cot-cli/Cargo.toml
tracing.workspace = true
tracing-subscriber = { workspace = true, features = ["env-filter"] }
serde = { workspace = true, features = ["derive"] }
serde_json = {workspace = true}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit

Suggested change
serde_json = {workspace = true}
serde_json = { workspace = true }

Comment thread cot/src/metadata.rs

/// Metadata describing the commands exposed by a Cot project binary.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ProjectMetadata {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If we define some custom metadata format, we should add some versioning here to make sure that the versions of cot-cli and metadata's match. Note that this can just be an integer increased each time any of the field change; this doesn't necessarily have to be the cot version itself.

-q, --quiet... Decrease logging verbosity
-h, --help Print help
-V, --version Print version
--release

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why did the formatting change so much here? IS this a result of adding a long param description, or something else?

Comment thread cot/src/metadata.rs
use serde::{Deserialize, Serialize};

/// Flag used to ask a Cot project binary to print its CLI metadata as JSON.
pub const METADATA_FLAG: &str = "--metadata";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think this option should be renamed to something that is unlikely to conflict with something that could be defined by the user. Maybe something more in the line of --cot-internal-cli-metadata?

Comment thread cot-cli/src/project.rs
bail!(msg);
}

let metadata: ProjectMetadata = serde_json::from_slice(&output.stdout).with_context(|| {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't think this is the right thing to do. Somebody could create a Cot binary without the entire metadata machinery - I'm doing something like this in this project, where the Cot server is only a small part inside a bigger binary, and I don't use any of the typical Cot machinery to run the server.

I don't think cot-cli should fail in case it cannot extract the metadata from the binary. Maybe we could just display an error message, but proceed as normal with the rest of the binary operation?

I'm also wondering if it won't be surprising for the users if the cot-cli actually runs the project binary when attempting to use unknown subcommands. What do you think?

Comment thread cot-cli/src/project.rs

let output = std::process::Command::new(binary_path)
.arg(METADATA_FLAG)
.output()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We should probably have some timeout to run the binary to avoid awkward silence when the binary doesn't actually recognize the metadata parameter, or hang for any other reason. This would be a very bad UX for cot-cli.

@ElijahAhianyo ElijahAhianyo mentioned this pull request Jul 30, 2026
6 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-deps Area: Dependencies C-cli Crate: cot-cli (issues and Pull Requests related to Cot CLI) C-lib Crate: cot (main library crate)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants