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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/).

### Added

- `forge tui` and bare `forge` under `--features tui` launch a ratatui terminal dashboard over the shared `commands::services` scan model, with artifacts, provenance, projects/ontology placeholder, sources/watch/find panes, and a command palette.
- Model-level qualifier resolution for rules and agents (PROV-0005 Phase 1). Assembly now resolves the full `user/` > `provider/<model>/` > `provider/` > base precedence: a file at `rules/<provider>/<model-id>/Rule.md` overrides the base for that provider and model, and the long-documented `user/` overlay is finally wired for rules and agents too. Each provider gains a default `model` in `defaults.yaml` (an exact ID from `config/models.yaml`); `forge assemble --model <ID>` and `forge install --model <ID>` override it for providers that list that model. Qualifier directory names are validated as exact model IDs: model IDs are no longer split into segments, so a directory named `4` or `6` (from `claude-opus-4-6`) is no longer a valid qualifier, and a model-only file under `rules/<provider>/<model-id>/` is collected instead of silently dropped. An unrecognized model-qualifier subdirectory is skipped with a warning rather than dropped silently. Skill overlays, recording the model in `.manifest`/provenance sidecars, and the per-model release matrix are deferred to Phase 2. (#60)
- `forge drift --target <BASE>` verifies a module's assembled `build/` against where it was deployed, scoped to the module's own files. It mirrors `forge install --target`: each provider's `build/<provider>` is compared to `<BASE>/<provider-target>`, so files built but not yet deployed surface as local-only, deployment edits surface as frontmatter/body drift, and this module's deployed files (per the target `.manifest` plus provenance attribution) that are no longer built surface as drift. Unlike `--upstream` against a multi-module tree such as `~/.claude`, other modules' files are never reported. `--target` and `--upstream` are mutually exclusive. (#61)
- `forge validate` sanity-checks Claude Code plugin scaffolding when `.claude-plugin/plugin.json` is present: each manifest (`plugin.json`, `.claude-plugin/marketplace.json`, `hooks/hooks.json`) must be valid JSON, and every hook script referenced via `${CLAUDE_PLUGIN_ROOT}` must exist and be executable (the most common cause of hooks silently not firing). It deliberately makes no assertions about the plugin or marketplace field schema, so it does not break when Claude Code's plugin format changes. Non-plugin modules are unaffected: the check runs only when `plugin.json` exists. (#59)
Expand Down
7 changes: 6 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ validate = ["dep:rust-embed"]
deploy = []
embed = ["dep:rust-embed"]
dashboard = ["full", "dep:axum", "dep:tokio", "dep:askama", "dep:tower-http", "dep:open"]
tui = ["dep:ratatui", "dep:crossterm"]

[dependencies]
# cli
Expand Down Expand Up @@ -58,6 +59,10 @@ askama = { version = "0.13", features = ["urlencode"], optional = true }
tower-http = { version = "0.6", features = ["set-header"], optional = true }
open = { version = "5", optional = true }

# tui (optional, excluded from full)
ratatui = { version = "0.30", optional = true }
crossterm = { version = "0.29", optional = true }

[build-dependencies]
sha2 = "0.10"

Expand All @@ -71,7 +76,7 @@ unsafe_code = "forbid"

[lints.rust.unexpected_cfgs]
level = "deny"
check-cfg = ["cfg(ci)", "cfg(feature, values(\"dashboard\"))"]
check-cfg = ["cfg(ci)", "cfg(feature, values(\"dashboard\", \"tui\"))"]

[lints.clippy]
all = { level = "warn", priority = -1 }
Expand Down
10 changes: 6 additions & 4 deletions src/cli/drift/scope/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ fn deploy_owned_file(
content: &str,
source_uri: &str,
) {
use std::fmt::Write as _;
write(&deployed_base.join(relative), content);

let artifact = std::path::Path::new(relative);
Expand All @@ -32,10 +33,11 @@ fn deploy_owned_file(

let manifest_path = deployed_base.join(".manifest");
let mut manifest_yaml = std::fs::read_to_string(&manifest_path).unwrap_or_default();
manifest_yaml.push_str(&format!(
"{relative}:\n fingerprint: {digest}\n provenance: {provenance_relative}\n",
digest = manifest::content_sha256(content)
));
let digest = manifest::content_sha256(content);
let _ = write!(
manifest_yaml,
"{relative}:\n fingerprint: {digest}\n provenance: {provenance_relative}\n"
);
std::fs::write(&manifest_path, manifest_yaml).unwrap();
}

Expand Down
31 changes: 27 additions & 4 deletions src/cli/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
mod assemble;
mod config;
pub(crate) mod config;
mod copy;
#[cfg(feature = "dashboard")]
mod dashboard;
Expand All @@ -12,11 +12,13 @@ mod output;
mod provenance;
mod release;
mod validate;
mod watchlist;
pub(crate) mod watchlist;

#[cfg(test)]
mod tests;

#[cfg(not(feature = "tui"))]
use clap::CommandFactory;
use clap::{Parser, Subcommand};
use commands::error::Error;
use commands::result::ActionResult;
Expand All @@ -25,7 +27,7 @@ use commands::result::ActionResult;
#[command(name = "forge", about = "Forge module toolkit", version)]
struct Cli {
#[command(subcommand)]
command: Command,
command: Option<Command>,

/// Output results as JSON
#[arg(long, global = true)]
Expand All @@ -34,6 +36,10 @@ struct Cli {

#[derive(Subcommand)]
enum Command {
/// Launch the terminal dashboard
#[cfg(feature = "tui")]
Tui,

/// Initialize a new forge module with required files and schemas
Init {
/// Directory to scaffold the new module into (created if missing).
Expand Down Expand Up @@ -280,7 +286,13 @@ enum WatchAction {
pub fn run() -> i32 {
let args = Cli::parse();

let (result, verb) = match args.command {
let Some(command) = args.command else {
return bare();
};

let (result, verb) = match command {
#[cfg(feature = "tui")]
Command::Tui => return crate::tui::run(),
Command::Init { target } => (init::execute(&target), "initialized"),
Command::Install {
source,
Expand Down Expand Up @@ -373,6 +385,17 @@ pub fn run() -> i32 {
report(result, args.json, verb)
}

#[cfg(feature = "tui")]
fn bare() -> i32 {
crate::tui::run()
}

#[cfg(not(feature = "tui"))]
fn bare() -> i32 {
eprintln!("{}", Cli::command().render_help());
2
}

/// Collapse a subcommand's `Result<exit_code, _>` into a process exit code,
/// printing a `fatal:` line on `Err`.
fn exit_code<E: std::fmt::Display>(result: Result<i32, E>) -> i32 {
Expand Down
2 changes: 2 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
mod cli;
#[cfg(feature = "tui")]
mod tui;

fn main() {
let exit_code = cli::run();
Expand Down
Loading