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
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ name: CI
on:
pull_request:
branches: [ "main" ]
paths:
- '**/*.rs'
- '**/*.toml'
- 'Cargo.lock'

env:
CARGO_TERM_COLOR: always
Expand Down
10 changes: 7 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
## General

Always prefer `cargo clippy` over cargo check
Always format the code with `cargo fmt` and run the test suite with `cargo test` before finalizing
Always prefer `cargo add` over manually editing Cargo.toml
Always prefer `cargo clippy` over cargo check.

Always format the code with `cargo fmt` and run the test suite with `cargo test` before finalizing if any rust code was
touched.

Always prefer `cargo add` over manually editing `Cargo.toml`.

Always prefer enums over strings when there's a clear set of valid values.
106 changes: 104 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,104 @@
# cf-cli
CyberFabric repositories automation tool
# CyberFabric CLI

Command-line interface for development and management of CyberFabric modules.

## Quickstart

### Prerequisites

- Rust toolchain with `cargo`
- A local clone of this repository

### Install the CLI

This workspace exposes two binaries:

- `cyberfabric`
- `cargo-cyberfabric` for the `cargo cyberfabric ...` invocation form

Install both from the repository root:

```bash
cargo install --path crates/cli --bin cyberfabric --bin cargo-cyberfabric
```

After installation, you can use either form:

```bash
cyberfabric --help
```

```bash
cargo cyberfabric --help
```

For local development without installing:

```bash
cargo run -p cli -- --help
```

## What the CLI can do

The current CLI surface is centered on CyberFabric workspace setup, configuration, code generation, and execution.

### Workspace scaffolding

- `mod init` initializes a new CyberFabric workspace from a template
- `mod add` adds module templates such as `background-worker`, `api-db-handler`, and `rest-gateway`

### Configuration management

- `config mod list` inspects available and configured modules
- `config mod add` and `config mod rm` manage module entries in the YAML config
- `config mod db add|edit|rm` manages module-level database settings
- `config db add|edit|rm` manages shared database server definitions

You need to provide the path to the configuration file with the `-c` flag. `-c config/quickstart.yml`

### Build and run generated servers

- `build` generates a runnable Cargo project under `.cyberfabric/` and builds it based on the `-c` configuration
provided.
- `run` generates the same project and runs it. You can provide `-w` to enable watch mode and/or `--otel` to enable
OpenTelemetry.

### Source inspection

- `docs` resolves Rust source for crates, modules, and items from the workspace, local cache, or `crates.io`

### Tool bootstrap

- `tools` installs or upgrades `rustup`, `rustfmt`, and `clippy`

### Current placeholders

- `lint` is declared but not implemented yet
- `test` is declared but not implemented yet

## Typical usage flow

Create a workspace, add a module, configure it, and run it:

```bash
cyberfabric mod init /tmp/cf-demo
cyberfabric mod add background-worker -p /tmp/cf-demo
cyberfabric config mod add background-worker -p /tmp/cf-demo -c /tmp/cf-demo/config/quickstart.yml
cyberfabric run -p /tmp/cf-demo -c /tmp/cf-demo/config/quickstart.yml
```

The `-p` is to specify the path. If you don't provide it, the default will be the current directory.

## Command overview

For the full command surface, arguments, and examples, check [SKILLS.md](SKILLS.md).

## License

This project is licensed under the Apache License, Version 2.0.

- Full license text: `LICENSE`
- License URL: <http://www.apache.org/licenses/LICENSE-2.0>

Unless required by applicable law or agreed to in writing, the software is distributed on an `AS IS` basis, without
warranties or conditions of any kind.
24 changes: 23 additions & 1 deletion crates/cli/src/common.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
use anyhow::Context;
use clap::Args;
use clap::{Args, ValueEnum};
use module_parser::{
CargoToml, CargoTomlDependencies, CargoTomlDependency, Config, ConfigModuleMetadata,
get_dependencies, get_module_name_from_crate,
};
use std::collections::HashMap;
use std::fmt::{self, Display};
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
Expand Down Expand Up @@ -49,6 +50,27 @@ pub struct BuildRunArgs {
pub clean: bool,
}

#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, ValueEnum)]
pub enum Registry {
#[default]
#[value(name = "crates.io")]
CratesIo,
}

impl Registry {
pub const fn as_str(self) -> &'static str {
match self {
Self::CratesIo => "crates.io",
}
}
}

impl Display for Registry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}

impl BuildRunArgs {
pub fn resolve_workspace_and_config(&self) -> anyhow::Result<(PathBuf, PathBuf)> {
let path = self.path_config.resolve_path()?;
Expand Down
37 changes: 19 additions & 18 deletions crates/cli/src/config/modules/list.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use super::{SYSTEM_REGISTRY_MODULES, SystemRegistryModule, load_config, resolve_modules_context};
use crate::common::PathConfigArgs;
use crate::common::{PathConfigArgs, Registry};
use crate::config::app_config::ModuleConfig;
use anyhow::{Context, bail};
use clap::Args;
Expand Down Expand Up @@ -27,9 +27,11 @@ pub struct ListArgs {
/// Show all information related to the module.
#[arg(short = 'v', long)]
verbose: bool,
/// Registry to query when verbose mode is enabled.
#[arg(long, default_value = "crates.io")]
registry: String,
/// Registry to query for system-crate metadata. Only consulted when both
/// `--system` and `--verbose` are enabled; `--verbose` alone does not query
/// any registry. Defaults to `crates.io`.
#[arg(long, value_enum, default_value_t = Registry::CratesIo)]
registry: Registry,
}

impl ListArgs {
Expand All @@ -42,19 +44,13 @@ impl ListArgs {
if self.system {
println!("System crates:");
if self.verbose {
if self.registry != "crates.io" {
let registry = &self.registry;
bail!(
"unsupported registry '{registry}'. Only 'crates.io' is currently supported"
);
}

let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.context("failed to build tokio runtime for registry queries")?;

let metadata_by_crate = runtime.block_on(fetch_all_crates_io_metadata())?;
let metadata_by_crate =
runtime.block_on(fetch_all_registry_metadata(self.registry))?;

for module in SYSTEM_REGISTRY_MODULES {
let Some(metadata) = metadata_by_crate.get(module.crate_name) else {
Expand Down Expand Up @@ -208,7 +204,9 @@ struct CrateVersion {
features: BTreeMap<String, Vec<String>>,
}

async fn fetch_all_crates_io_metadata() -> anyhow::Result<HashMap<&'static str, RegistryMetadata>> {
async fn fetch_all_registry_metadata(
registry: Registry,
) -> anyhow::Result<HashMap<&'static str, RegistryMetadata>> {
let semaphore = std::sync::Arc::new(tokio::sync::Semaphore::new(4));
let client = Client::builder()
.user_agent("cyberfabric-cli")
Expand All @@ -225,7 +223,7 @@ async fn fetch_all_crates_io_metadata() -> anyhow::Result<HashMap<&'static str,
.acquire_owned()
.await
.context("failed to acquire registry fetch permit")?;
let metadata = fetch_crates_io_metadata(&cloned_client, module)
let metadata = fetch_registry_metadata(&cloned_client, registry, module)
.await
.with_context(|| format!("failed to fetch metadata for '{}'", module.crate_name))?;
Ok::<_, anyhow::Error>((module.crate_name, metadata))
Expand All @@ -241,11 +239,12 @@ async fn fetch_all_crates_io_metadata() -> anyhow::Result<HashMap<&'static str,
Ok(metadata_by_crate)
}

async fn fetch_crates_io_metadata(
async fn fetch_registry_metadata(
client: &Client,
registry: Registry,
module: SystemRegistryModule,
) -> anyhow::Result<RegistryMetadata> {
let crate_url = format!("https://crates.io/api/v1/crates/{}", module.crate_name);
let crate_url = format!("https://{registry}/api/v1/crates/{}", module.crate_name);
let crate_response = client
.get(&crate_url)
.send()
Expand All @@ -264,7 +263,8 @@ async fn fetch_crates_io_metadata(
.find(|version| version.num == latest_version)
.map_or_else(Vec::new, |version| version.features.into_keys().collect());

let module_rs_content = fetch_module_rs_content(client, module, &latest_version).await?;
let module_rs_content =
fetch_module_rs_content(client, registry, module, &latest_version).await?;
let module_metadata = parse_module_rs_source(&module_rs_content)
.with_context(|| format!("invalid src/module.rs for {}", module.crate_name))?;

Expand All @@ -278,11 +278,12 @@ async fn fetch_crates_io_metadata(

async fn fetch_module_rs_content(
client: &Client,
registry: Registry,
module: SystemRegistryModule,
latest_version: &str,
) -> anyhow::Result<String> {
let download_url = format!(
"https://crates.io/api/v1/crates/{}/{}/download",
"https://{registry}/api/v1/crates/{}/{}/download",
module.crate_name, latest_version
);
let crate_archive = client
Expand Down
Loading