Skip to content
Open
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 Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ minijinja = "1.0"
serde_yaml = "0.9.34"
async-trait = "0.1"
futures = "0.3.33"
thiserror = "2"

[features]
hardware-wallet = ["dep:hidapi", "dep:trezor-client"]
Expand Down
1 change: 1 addition & 0 deletions src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pub mod ai_contract_suggest;
pub mod ai_debug;
pub mod ai_deploy_docs;
pub mod ai_deployment_test;
pub mod ai_doc_qa;
pub mod ai_error;
pub mod ai_feedback;
pub mod ai_ide;
Expand Down
110 changes: 100 additions & 10 deletions src/commands/template.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,24 @@ pub enum TemplateCommands {
/// Force refresh of remote registry, ignoring cached copy
#[arg(long)]
refresh: bool,
/// Maximum number of results to show per page. Omit to show all matches.
#[arg(long)]
limit: Option<usize>,
/// Resume after this pagination cursor (from a previous page's "Next cursor")
#[arg(long)]
cursor: Option<String>,
},
/// List all available templates
List {
/// Emit a machine-readable JSON object instead of the human-readable output
#[arg(long)]
json: bool,
/// Maximum number of templates to show per page. Omit to show all.
#[arg(long)]
limit: Option<usize>,
/// Resume after this pagination cursor (from a previous page's "Next cursor")
#[arg(long)]
cursor: Option<String>,
},
/// Show details of a specific template
Show {
Expand Down Expand Up @@ -241,14 +253,20 @@ pub async fn handle(cmd: TemplateCommands) -> Result<()> {
)
.await
}
TemplateCommands::List { json } => list(json).await,
TemplateCommands::List {
json,
limit,
cursor,
} => list(json, limit, cursor).await,
TemplateCommands::Search {
query,
tags,
verified,
min_quality,
refresh,
} => search(query, tags, verified, min_quality, refresh).await,
limit,
cursor,
} => search(query, tags, verified, min_quality, refresh, limit, cursor).await,
TemplateCommands::Show { name } => show(name).await,
TemplateCommands::Remove { name, purge } => remove(name, purge).await,
TemplateCommands::Init => init(),
Expand Down Expand Up @@ -430,16 +448,53 @@ async fn publish(
Ok(())
}

async fn list(json: bool) -> Result<()> {
/// Default number of results shown per page when pagination is requested via
/// `--cursor` without an explicit `--limit`.
const DEFAULT_PAGE_LIMIT: usize = 20;

/// Print the "Shown X of Y" / "Next cursor" footer for a paginated command,
/// when pagination was requested at all.
fn print_pagination_footer<T>(page: Option<&templates::Page<T>>) {
if let Some(page) = page {
println!();
p::kv("Shown", &format!("{} of {}", page.items.len(), page.total));
if let Some(next) = &page.next_cursor {
p::info(&format!(
"More results available — pass --cursor {} to continue",
next
));
}
}
}

async fn list(json: bool, limit: Option<usize>, cursor: Option<String>) -> Result<()> {
use crate::utils::templates::{check_template_compatibility, CompatibilityStatus};

let registry = templates::load_registry().await?;
let emit_json = json || output::is_json_mode_enabled();

// Pagination only kicks in when the caller opts in via --limit or
// --cursor, so plain `template list` keeps showing everything.
let page = match limit.or(cursor.is_some().then_some(DEFAULT_PAGE_LIMIT)) {
Some(limit) => Some(templates::paginate(
&registry.templates,
cursor.as_deref(),
limit,
|t| t.name.as_str(),
)?),
None => None,
};
let shown: &[templates::TemplateEntry] = match &page {
Some(page) => &page.items,
None => &registry.templates,
};

if emit_json {
#[derive(serde::Serialize)]
struct TemplateListResponse {
template_count: usize,
shown_count: usize,
next_cursor: Option<String>,
templates: Vec<TemplateSummary>,
}

Expand All @@ -454,8 +509,9 @@ async fn list(json: bool) -> Result<()> {
compatible: bool,
}

let templates: Vec<TemplateSummary> = registry
.templates
let template_count = registry.templates.len();
let next_cursor = page.as_ref().and_then(|p| p.next_cursor.clone());
let templates: Vec<TemplateSummary> = shown
.iter()
.map(|template| {
let compatible = matches!(
Expand All @@ -475,7 +531,9 @@ async fn list(json: bool) -> Result<()> {
.collect();

return output::print_json(&TemplateListResponse {
template_count: templates.len(),
template_count,
shown_count: templates.len(),
next_cursor,
templates,
});
}
Expand All @@ -484,8 +542,12 @@ async fn list(json: bool) -> Result<()> {
p::info("No templates found. Publish one with: starforge template publish <path>");
return Ok(());
}
if shown.is_empty() {
p::info("No more templates on this page.");
return Ok(());
}

for (i, template) in registry.templates.iter().enumerate() {
for (i, template) in shown.iter().enumerate() {
let compat_badge = match check_template_compatibility(template) {
CompatibilityStatus::Compatible => "[COMPATIBLE]",
CompatibilityStatus::TooOld { .. } | CompatibilityStatus::TooNew { .. } => {
Expand All @@ -511,20 +573,25 @@ async fn list(json: bool) -> Result<()> {
if let Some(path) = template.path.as_ref() {
p::kv("Path", path);
}
if i + 1 < registry.templates.len() {
if i + 1 < shown.len() {
println!();
}
}

print_pagination_footer(page.as_ref());

Ok(())
}

#[allow(clippy::too_many_arguments)]
async fn search(
query: String,
tags: Option<String>,
verified: bool,
min_quality: u8,
refresh: bool,
limit: Option<usize>,
cursor: Option<String>,
) -> Result<()> {
use crate::utils::templates::{check_template_compatibility, CompatibilityStatus};
let tag_list: Vec<String> = tags
Expand Down Expand Up @@ -580,10 +647,31 @@ async fn search(
return Ok(());
}

// Pagination only kicks in when the caller opts in via --limit or
// --cursor, so plain `template search` keeps showing every match.
let page = match limit.or(cursor.is_some().then_some(DEFAULT_PAGE_LIMIT)) {
Some(limit) => Some(templates::paginate(
&results,
cursor.as_deref(),
limit,
|r| r.entry.name.as_str(),
)?),
None => None,
};
let shown: &[templates::SearchResult] = match &page {
Some(page) => &page.items,
None => &results,
};

p::kv("Matches", &results.len().to_string());
println!();

for (i, result) in results.iter().enumerate() {
if shown.is_empty() {
p::info("No more results on this page.");
return Ok(());
}

for (i, result) in shown.iter().enumerate() {
let template = &result.entry;
let compat_badge = match check_template_compatibility(template) {
CompatibilityStatus::Compatible => "[COMPATIBLE]",
Expand Down Expand Up @@ -619,11 +707,13 @@ async fn search(
);
}
p::kv("Source", &template.source.to_string());
if i + 1 < results.len() {
if i + 1 < shown.len() {
println!();
}
}

print_pagination_footer(page.as_ref());

Ok(())
}

Expand Down
20 changes: 10 additions & 10 deletions src/utils/database.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ pub trait Migration: Send + Sync {
fn description(&self) -> &str;

/// Apply the migration (upgrade)
fn up(&self, conn: &mut Connection) -> Result<()>;
fn up(&self, conn: &Connection) -> Result<()>;

/// Rollback the migration (downgrade)
fn down(&self, conn: &mut Connection) -> Result<()>;
fn down(&self, conn: &Connection) -> Result<()>;
}

/// Record of an applied migration in the database
Expand Down Expand Up @@ -220,9 +220,9 @@ impl Database {
.ok_or_else(|| anyhow::anyhow!("Migration version {} not found", version))?;

let tx = self.conn.unchecked_transaction()?;

// Apply the migration
match migration.up(&mut tx) {
match migration.up(&tx) {
Ok(()) => {
// Record the migration
let checksum = self.compute_migration_checksum(version, migration.description())?;
Expand Down Expand Up @@ -273,9 +273,9 @@ impl Database {
.ok_or_else(|| anyhow::anyhow!("Migration version {} not found", version))?;

let tx = self.conn.unchecked_transaction()?;

// Rollback the migration
match migration.down(&mut tx) {
match migration.down(&tx) {
Ok(()) => {
// Remove the migration record
tx.execute(
Expand Down Expand Up @@ -1110,12 +1110,12 @@ impl Migration for MigrationV1 {
"initial_schema"
}

fn up(&self, conn: &mut Connection) -> Result<()> {
fn up(&self, conn: &Connection) -> Result<()> {
// This is a no-op since the initial schema is already applied in SCHEMA
Ok(())
}
fn down(&self, conn: &mut Connection) -> Result<()> {

fn down(&self, conn: &Connection) -> Result<()> {
// Rollback: drop all tables
conn.execute_batch(
"DROP TABLE IF EXISTS events;
Expand Down
1 change: 1 addition & 0 deletions src/utils/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub mod ai_debug_enhancement;
pub mod ai_debugger;
pub mod ai_deployment_planner;
pub mod ai_deployment_testing;
pub mod ai_doc_qa;
pub mod ai_docs;
pub mod ai_documentation_assistant;
pub mod ai_error_handler;
Expand Down
Loading
Loading