Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,14 +105,25 @@ See [crates/codra-cli/README.md](crates/codra-cli/README.md).

## Installable CLI roadmap

The Rust CLI will be installable globally via npm (not published yet):
The `@codra/cli` package is publish-ready work in progress and is not published to npm yet. Once maintainers verify the release tarball includes every selected platform binary, users will be able to install Codra globally:

```bash
npm install -g @codra/cli # coming soon
codra run --task summarize-context --jsonl
pnpm add -g @codra/cli
npm install -g @codra/cli
codra --help
codra
```

The [`@codra/cli`](packages/codra-npm-cli/) package is a thin Node wrapper that spawns the native `codra` binary built from `codra-cli`. Multi-platform npm distribution is in progress (linux/macOS/Windows targets); a manual [release workflow](.github/workflows/codra-cli-release.yml) packages platform binaries before publish. Intel macOS (`darwin-x64`) is optional in dry runs because `macos-13` runners can queue for a long time; npm publish stays guarded and off by default (see [packages/codra-npm-cli/README.md](packages/codra-npm-cli/README.md)).
Today, local development can run the same command surface from the Rust crate or npm wrapper:

```bash
cargo run -p codra-cli --
cargo run -p codra-cli -- doctor
cargo run -p codra-cli -- init
cargo run -p codra-cli -- run --task summarize-context --jsonl
```

The [`@codra/cli`](packages/codra-npm-cli/) package is a thin Node wrapper that spawns the native `codra` binary built from `codra-cli`. Multi-platform npm distribution targets Linux, macOS, and Windows; the manual [release workflow](.github/workflows/codra-cli-release.yml) packages platform binaries before guarded publish. npm publish remains off by default and must not run until tarball contents are verified (see [packages/codra-npm-cli/README.md](packages/codra-npm-cli/README.md)).

## Roadmap

Expand Down
15 changes: 15 additions & 0 deletions crates/codra-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@ Local-first Codra CLI with JSONL event protocol and GitHub context adapter.
# Build
cargo build -p codra-cli

# Codex-style terminal entrypoint
codra
codra --help

# Project setup and environment checks
codra init
codra init --force
codra init --dry-run
codra doctor
codra doctor --json

# JSONL event stream (no AI keys required)
codra run --task review-pr --jsonl
codra run --task explain-issue --jsonl
Expand All @@ -18,6 +29,10 @@ codra run --task summarize-context
codra run --task review-pr
```

`codra init` creates `CODRA.md`, `.codra/commands/`, and `.codra/agents/` starter files at the git root when available. It does not overwrite existing files unless `--force` is passed.

`codra doctor` checks git, cargo, Node, npm/pnpm, GitHub Actions environment, `GITHUB_TOKEN` presence without printing the value, Codra project files, the `codra` binary on `PATH`, and the npm platform key. Missing optional tools are warnings and exit 0.

## GitHub context

Real GitHub Actions mode is enabled only when `GITHUB_ACTIONS=true`. If `GITHUB_EVENT_PATH` is set outside Actions, the CLI parses it as a local fixture and keeps `mode` as `local`.
Expand Down
178 changes: 178 additions & 0 deletions crates/codra-cli/src/doctor.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
use std::env;
use std::path::PathBuf;

use serde::Serialize;

use crate::project;

#[derive(Debug, Serialize)]
struct DoctorReport {
checks: Vec<DoctorCheck>,
}

#[derive(Debug, Serialize)]
struct DoctorCheck {
name: &'static str,
status: &'static str,
detail: String,
}

pub fn execute_doctor(args: &[String]) -> Result<(), String> {
if args.iter().any(|arg| arg == "--help" || arg == "-h") {
println!("codra doctor [--json]");
println!(" Checks local environment readiness without printing secret values.");
return Ok(());
}

let json = parse_doctor_args(args)?;
let report = collect_report();

if json {
let body = serde_json::to_string_pretty(&report).map_err(|err| err.to_string())?;
println!("{body}");
} else {
print_human_report(&report);
}

Ok(())
}

fn parse_doctor_args(args: &[String]) -> Result<bool, String> {
let mut json = false;
for arg in args {
match arg.as_str() {
"--json" => json = true,
flag if flag.starts_with("--") => return Err(format!("unknown flag: {flag}")),
other => return Err(format!("unexpected argument: {other}")),
}
}
Ok(json)
}

fn collect_report() -> DoctorReport {
let cwd = project::current_dir();
let root = project::project_root();
let git_installed = project::command_exists("git");
let inside_git = git_installed && project::is_inside_git_repo();
let branch = if inside_git {
project::git_branch().unwrap_or_else(|| "detached".to_string())
} else {
"n/a".to_string()
};
let working_tree = if inside_git {
match project::git_status_short() {
Some(value) if !value.trim().is_empty() => "dirty".to_string(),
_ => "clean".to_string(),
}
} else {
"n/a".to_string()
};

DoctorReport {
checks: vec![
ok("current directory", cwd.display().to_string()),
check("git installed", git_installed, detail_or_missing("git")),
check("inside git repo", inside_git, yes_no(inside_git)),
ok("branch", branch),
ok("working tree", working_tree),
check(
"cargo available",
project::command_exists("cargo"),
detail_or_missing("cargo"),
),
check(
"node available",
project::command_exists("node"),
detail_or_missing("node"),
),
check(
"npm available",
project::command_exists("npm"),
detail_or_missing("npm"),
),
check(
"pnpm available",
project::command_exists("pnpm"),
detail_or_missing("pnpm"),
),
check(
"GitHub Actions env",
env::var("GITHUB_ACTIONS")
.map(|value| value == "true")
.unwrap_or(false),
yes_no(
env::var("GITHUB_ACTIONS")
.map(|value| value == "true")
.unwrap_or(false),
),
),
check(
"GITHUB_TOKEN present",
env::var("GITHUB_TOKEN")
.map(|value| !value.is_empty())
.unwrap_or(false),
yes_no(
env::var("GITHUB_TOKEN")
.map(|value| !value.is_empty())
.unwrap_or(false),
),
),
check(
"CODRA.md exists",
root.join("CODRA.md").exists(),
path_detail(root.join("CODRA.md")),
),
check(
".codra directory exists",
root.join(".codra").is_dir(),
path_detail(root.join(".codra")),
),
check(
"codra binary on PATH",
project::which("codra").is_some(),
project::which("codra").unwrap_or_else(|| "not found".to_string()),
),
ok("npm platform key", project::npm_platform_key()),
],
}
}

fn print_human_report(report: &DoctorReport) {
println!("Codra doctor");
println!();
for check in &report.checks {
println!("{:<24} {:<7} {}", check.name, check.status, check.detail);
}
}

fn ok(name: &'static str, detail: String) -> DoctorCheck {
DoctorCheck {
name,
status: "ok",
detail,
}
}

fn check(name: &'static str, ok: bool, detail: String) -> DoctorCheck {
DoctorCheck {
name,
status: if ok { "ok" } else { "warn" },
detail,
}
}

fn yes_no(value: bool) -> String {
if value {
"yes".to_string()
} else {
"no".to_string()
}
}

fn detail_or_missing(program: &str) -> String {
project::which(program).unwrap_or_else(|| "not found".to_string())
}

fn path_detail(path: PathBuf) -> String {
path.display().to_string()
}
Loading
Loading