From 90aef094d7e109538cc613c8f3ce9d365d7f8890 Mon Sep 17 00:00:00 2001 From: rroskam Date: Sun, 15 Feb 2026 14:18:22 -0500 Subject: [PATCH 1/2] refactor: remove check/ready commands, examples, and unused deps - Remove `diecut check` and `diecut ready` commands (src/check.rs, src/ready.rs, commands/check.rs, commands/ready.rs) - Remove examples/ directory (templates will live in diecut-templates repo) - Remove unused dependencies: rhai, similar - Remove CI examples smoke test job and external-templates workflow - Fix HookError diagnostic help text (was referencing Rhai) --- .github/workflows/ci.yml | 17 -- .github/workflows/external-templates.yml | 39 ----- Cargo.lock | 155 ------------------ Cargo.toml | 2 - examples/python-pkg/README.md | 19 --- examples/python-pkg/diecut.toml | 37 ----- .../python-pkg/template/pyproject.toml.tera | 14 -- examples/python-pkg/template/src/__init__.py | 0 examples/python-pkg/template/src/cli.py.tera | 14 -- .../python-pkg/template/tests/__init__.py | 0 examples/rust-cli/README.md | 19 --- examples/rust-cli/diecut.toml | 33 ---- examples/rust-cli/template/.gitignore | 1 - examples/rust-cli/template/Cargo.toml.tera | 10 -- examples/rust-cli/template/src/main.rs.tera | 14 -- src/check.rs | 124 -------------- src/cli.rs | 14 -- src/commands/check.rs | 45 ----- src/commands/mod.rs | 2 - src/commands/ready.rs | 59 ------- src/error.rs | 2 +- src/lib.rs | 2 - src/main.rs | 2 - src/ready.rs | 47 ------ 24 files changed, 1 insertion(+), 670 deletions(-) delete mode 100644 .github/workflows/external-templates.yml delete mode 100644 examples/python-pkg/README.md delete mode 100644 examples/python-pkg/diecut.toml delete mode 100644 examples/python-pkg/template/pyproject.toml.tera delete mode 100644 examples/python-pkg/template/src/__init__.py delete mode 100644 examples/python-pkg/template/src/cli.py.tera delete mode 100644 examples/python-pkg/template/tests/__init__.py delete mode 100644 examples/rust-cli/README.md delete mode 100644 examples/rust-cli/diecut.toml delete mode 100644 examples/rust-cli/template/.gitignore delete mode 100644 examples/rust-cli/template/Cargo.toml.tera delete mode 100644 examples/rust-cli/template/src/main.rs.tera delete mode 100644 src/check.rs delete mode 100644 src/commands/check.rs delete mode 100644 src/commands/ready.rs delete mode 100644 src/ready.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ceb583e..2969e31 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,7 +5,6 @@ on: branches: [main] paths: - "src/**" - - "examples/**" - "Cargo.toml" - "Cargo.lock" - "justfile" @@ -14,7 +13,6 @@ on: branches: [main] paths: - "src/**" - - "examples/**" - "Cargo.toml" - "Cargo.lock" - "justfile" @@ -38,18 +36,3 @@ jobs: - name: Run checks run: just check - examples: - name: Example Template Smoke Tests - runs-on: ubuntu-latest - needs: [check] - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - - uses: extractions/setup-just@v2 - - uses: Swatinem/rust-cache@v2 - - name: Build diecut - run: just build - - name: Test examples/python-pkg - run: ./target/release/diecut new examples/python-pkg --defaults --data author=CI --output /tmp/out-python-pkg - - name: Test examples/rust-cli - run: ./target/release/diecut new examples/rust-cli --defaults --data author=CI --output /tmp/out-rust-cli diff --git a/.github/workflows/external-templates.yml b/.github/workflows/external-templates.yml deleted file mode 100644 index 7ef9152..0000000 --- a/.github/workflows/external-templates.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: External Template Compatibility - -on: - push: - branches: [main] - paths: - - "crates/**" - - "Cargo.toml" - - "Cargo.lock" - schedule: - - cron: "0 9 * * 1" # Weekly on Monday at 9am UTC - -env: - CARGO_TERM_COLOR: always - RUSTFLAGS: -Dwarnings - -jobs: - external-templates: - name: External Template Compatibility - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - - uses: extractions/setup-just@v2 - - uses: Swatinem/rust-cache@v2 - - name: Build diecut - run: just build - - name: Test cookiecutter-pypackage - run: | - git clone --depth 1 https://github.com/audreyfeldroy/cookiecutter-pypackage.git /tmp/cc-pypackage - ./target/release/diecut new /tmp/cc-pypackage --defaults --output /tmp/out-pypackage || echo "::warning::cookiecutter-pypackage failed (may use unsupported features)" - - name: Test cookiecutter-django - run: | - git clone --depth 1 https://github.com/cookiecutter/cookiecutter-django.git /tmp/cc-django - ./target/release/diecut new /tmp/cc-django --defaults --output /tmp/out-django || echo "::warning::cookiecutter-django failed (may use unsupported features)" - - name: Test cookiecutter-golang - run: | - git clone --depth 1 https://github.com/lacion/cookiecutter-golang.git /tmp/cc-golang - ./target/release/diecut new /tmp/cc-golang --defaults --output /tmp/out-golang || echo "::warning::cookiecutter-golang failed (may use unsupported features)" diff --git a/Cargo.lock b/Cargo.lock index f024dda..1764df0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,20 +17,6 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" -[[package]] -name = "ahash" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" -dependencies = [ - "cfg-if", - "const-random", - "getrandom 0.3.4", - "once_cell", - "version_check", - "zerocopy", -] - [[package]] name = "aho-corasick" version = "1.1.4" @@ -286,26 +272,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "const-random" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" -dependencies = [ - "const-random-macro", -] - -[[package]] -name = "const-random-macro" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" -dependencies = [ - "getrandom 0.2.17", - "once_cell", - "tiny-keccak", -] - [[package]] name = "content_inspector" version = "0.2.4" @@ -380,12 +346,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "crunchy" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" - [[package]] name = "crypto-common" version = "0.1.7" @@ -415,11 +375,9 @@ dependencies = [ "inquire", "miette", "regex-lite", - "rhai", "serde", "serde_json", "sha2", - "similar", "tempfile", "tera", "thiserror", @@ -553,18 +511,6 @@ dependencies = [ "wasi", ] -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "libc", - "r-efi", - "wasip2", -] - [[package]] name = "getrandom" version = "0.4.1" @@ -868,15 +814,6 @@ dependencies = [ "unicode-segmentation", ] -[[package]] -name = "no-std-compat" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" -dependencies = [ - "spin", -] - [[package]] name = "num-traits" version = "0.2.19" @@ -900,9 +837,6 @@ name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" -dependencies = [ - "portable-atomic", -] [[package]] name = "once_cell_polyfill" @@ -1041,12 +975,6 @@ dependencies = [ "siphasher", ] -[[package]] -name = "portable-atomic" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" - [[package]] name = "ppv-lite86" version = "0.2.21" @@ -1175,35 +1103,6 @@ version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" -[[package]] -name = "rhai" -version = "1.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f9ef5dabe4c0b43d8f1187dc6beb67b53fe607fff7e30c5eb7f71b814b8c2c1" -dependencies = [ - "ahash", - "bitflags 2.11.0", - "no-std-compat", - "num-traits", - "once_cell", - "rhai_codegen", - "smallvec", - "smartstring", - "thin-vec", - "web-time", -] - -[[package]] -name = "rhai_codegen" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4322a2a4e8cf30771dd9f27f7f37ca9ac8fe812dddd811096a98483080dabe6" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "rustc-demangle" version = "0.1.27" @@ -1363,12 +1262,6 @@ dependencies = [ "libc", ] -[[package]] -name = "similar" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" - [[package]] name = "siphasher" version = "1.0.2" @@ -1391,29 +1284,6 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" -[[package]] -name = "smartstring" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29" -dependencies = [ - "autocfg", - "static_assertions", - "version_check", -] - -[[package]] -name = "spin" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - [[package]] name = "strsim" version = "0.11.1" @@ -1507,12 +1377,6 @@ dependencies = [ "unicode-width 0.2.2", ] -[[package]] -name = "thin-vec" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "144f754d318415ac792f9d69fc87abbbfc043ce2ef041c60f16ad828f638717d" - [[package]] name = "thiserror" version = "2.0.18" @@ -1542,15 +1406,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "tiny-keccak" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" -dependencies = [ - "crunchy", -] - [[package]] name = "toml" version = "0.8.23" @@ -1765,16 +1620,6 @@ dependencies = [ "semver", ] -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index 64c9a11..2bda897 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,9 +30,7 @@ miette = { version = "7", features = ["fancy"] } walkdir = "2" console = "0.15" dirs = "6" -rhai = { version = "1", features = ["sync"] } tempfile = "3" sha2 = "0.10" -similar = "2" fs4 = "0.12" content_inspector = "0.2" diff --git a/examples/python-pkg/README.md b/examples/python-pkg/README.md deleted file mode 100644 index 40df76f..0000000 --- a/examples/python-pkg/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# python-pkg - -A diecut template for generating a Python package with pyproject.toml (hatchling). - -## Usage - -```bash -diecut new ./examples/python-pkg -o my-project -``` - -## Variables - -| Variable | Type | Default | Description | -|----------|------|---------|-------------| -| `project_name` | string | `my-package` | Project name | -| `description` | string | `A Python package` | Short description | -| `author` | string | | Author name | -| `python_version` | select | `3.12` | Minimum Python version | -| `use_cli` | bool | `false` | Include CLI entry point | diff --git a/examples/python-pkg/diecut.toml b/examples/python-pkg/diecut.toml deleted file mode 100644 index 61d92e9..0000000 --- a/examples/python-pkg/diecut.toml +++ /dev/null @@ -1,37 +0,0 @@ -[template] -name = "python-pkg" -version = "0.1.0" -description = "A Python package with pyproject.toml" - -[variables.project_name] -type = "string" -prompt = "Project name" -default = "my-package" - -[variables.description] -type = "string" -prompt = "Short description" -default = "A Python package" - -[variables.author] -type = "string" -prompt = "Author name" - -[variables.python_version] -type = "select" -prompt = "Minimum Python version" -choices = ["3.10", "3.11", "3.12", "3.13"] -default = "3.12" - -[variables.use_cli] -type = "bool" -prompt = "Include CLI entry point?" -default = false - -[files] -conditional = [ - { pattern = "src/cli.py*", when = "use_cli" }, -] - -[answers] -file = ".diecut-answers.toml" diff --git a/examples/python-pkg/template/pyproject.toml.tera b/examples/python-pkg/template/pyproject.toml.tera deleted file mode 100644 index 0ba40cb..0000000 --- a/examples/python-pkg/template/pyproject.toml.tera +++ /dev/null @@ -1,14 +0,0 @@ -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[project] -name = "{{ project_name }}" -version = "0.1.0" -description = "{{ description }}" -{% if author %}authors = [{ name = "{{ author }}" }] -{% endif %}requires-python = ">={{ python_version }}" -{% if use_cli %} -[project.scripts] -{{ project_name }} = "{{ project_name | replace(from="-", to="_") }}.cli:main" -{% endif %} diff --git a/examples/python-pkg/template/src/__init__.py b/examples/python-pkg/template/src/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/examples/python-pkg/template/src/cli.py.tera b/examples/python-pkg/template/src/cli.py.tera deleted file mode 100644 index dd12d8c..0000000 --- a/examples/python-pkg/template/src/cli.py.tera +++ /dev/null @@ -1,14 +0,0 @@ -"""CLI entry point for {{ project_name }}.""" - -import argparse - - -def main(): - parser = argparse.ArgumentParser(description="{{ description }}") - parser.add_argument("name", nargs="?", default="world", help="Name to greet") - args = parser.parse_args() - print(f"Hello, {args.name}!") - - -if __name__ == "__main__": - main() diff --git a/examples/python-pkg/template/tests/__init__.py b/examples/python-pkg/template/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/examples/rust-cli/README.md b/examples/rust-cli/README.md deleted file mode 100644 index 57af1d5..0000000 --- a/examples/rust-cli/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# rust-cli - -A diecut template for generating a minimal Rust CLI application using clap. - -## Usage - -```bash -diecut new ./examples/rust-cli -o my-project -``` - -## Variables - -| Variable | Type | Default | Description | -|----------|------|---------|-------------| -| `project_name` | string | `my-cli` | Project name | -| `description` | string | `A command-line application` | Short description | -| `author` | string | | Author name | -| `license` | select | `MIT` | License (MIT, Apache-2.0, or dual) | -| `rust_edition` | select | `2021` | Rust edition (2021 or 2024) | diff --git a/examples/rust-cli/diecut.toml b/examples/rust-cli/diecut.toml deleted file mode 100644 index 8ccecd8..0000000 --- a/examples/rust-cli/diecut.toml +++ /dev/null @@ -1,33 +0,0 @@ -[template] -name = "rust-cli" -version = "0.1.0" -description = "A minimal Rust CLI application" - -[variables.project_name] -type = "string" -prompt = "Project name" -default = "my-cli" - -[variables.description] -type = "string" -prompt = "Short description" -default = "A command-line application" - -[variables.author] -type = "string" -prompt = "Author name" - -[variables.license] -type = "select" -prompt = "License" -choices = ["MIT", "Apache-2.0", "MIT OR Apache-2.0"] -default = "MIT" - -[variables.rust_edition] -type = "select" -prompt = "Rust edition" -choices = ["2021", "2024"] -default = "2021" - -[answers] -file = ".diecut-answers.toml" diff --git a/examples/rust-cli/template/.gitignore b/examples/rust-cli/template/.gitignore deleted file mode 100644 index ea8c4bf..0000000 --- a/examples/rust-cli/template/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/target diff --git a/examples/rust-cli/template/Cargo.toml.tera b/examples/rust-cli/template/Cargo.toml.tera deleted file mode 100644 index 0295f7e..0000000 --- a/examples/rust-cli/template/Cargo.toml.tera +++ /dev/null @@ -1,10 +0,0 @@ -[package] -name = "{{ project_name }}" -version = "0.1.0" -edition = "{{ rust_edition }}" -description = "{{ description }}" -{% if author %}authors = ["{{ author }}"] -{% endif %}license = "{{ license }}" - -[dependencies] -clap = { version = "4", features = ["derive"] } diff --git a/examples/rust-cli/template/src/main.rs.tera b/examples/rust-cli/template/src/main.rs.tera deleted file mode 100644 index 47c8c7c..0000000 --- a/examples/rust-cli/template/src/main.rs.tera +++ /dev/null @@ -1,14 +0,0 @@ -use clap::Parser; - -#[derive(Parser)] -#[command(name = "{{ project_name }}", about = "{{ description }}", version)] -struct Cli { - /// Name to greet - #[arg(short, long, default_value = "world")] - name: String, -} - -fn main() { - let cli = Cli::parse(); - println!("Hello, {}!", cli.name); -} diff --git a/src/check.rs b/src/check.rs deleted file mode 100644 index 4e7ffb8..0000000 --- a/src/check.rs +++ /dev/null @@ -1,124 +0,0 @@ -use std::path::Path; - -use crate::adapter::resolve_template; -use crate::error::Result; - -pub struct CheckResult { - pub template_name: String, - pub variable_count: usize, - pub warnings: Vec, - pub errors: Vec, -} - -pub fn check_template(template_dir: &Path) -> Result { - let resolved = resolve_template(template_dir)?; - let config = &resolved.config; - - let mut warnings = resolved.warnings.clone(); - let mut errors = Vec::new(); - - if let Err(e) = config.validate() { - errors.push(format!("Config validation: {e}")); - } - - if !resolved.content_dir.exists() { - errors.push(format!( - "Template content directory not found: {}", - resolved.content_dir.display() - )); - } - - let suffix = &config.template.templates_suffix; - if resolved.content_dir.exists() { - validate_tera_files(&resolved.content_dir, suffix, &mut warnings, &mut errors); - } - - for cond in &config.files.conditional { - if let Err(e) = validate_tera_expression(&cond.when) { - errors.push(format!( - "Invalid conditional expression for pattern '{}': {e}", - cond.pattern - )); - } - } - - for (name, var) in &config.variables { - if let Some(when) = &var.when { - if let Err(e) = validate_tera_expression(when) { - errors.push(format!( - "Invalid 'when' expression for variable '{name}': {e}" - )); - } - } - if let Some(computed) = &var.computed { - if let Err(e) = validate_tera_template(computed) { - errors.push(format!( - "Invalid 'computed' expression for variable '{name}': {e}" - )); - } - } - } - - Ok(CheckResult { - template_name: config.template.name.clone(), - variable_count: config.variables.len(), - warnings, - errors, - }) -} - -fn validate_tera_files( - dir: &Path, - suffix: &str, - warnings: &mut Vec, - errors: &mut Vec, -) { - let walker = walkdir::WalkDir::new(dir) - .into_iter() - .filter_map(|e| e.ok()); - for entry in walker { - if !entry.file_type().is_file() { - continue; - } - let path = entry.path(); - let path_str = path.to_string_lossy(); - - if !suffix.is_empty() && !path_str.ends_with(suffix) { - continue; - } - - if crate::render::file::is_binary_file(path) { - continue; - } - - match std::fs::read_to_string(path) { - Ok(content) => { - let mut tera = tera::Tera::default(); - let template_name = path.strip_prefix(dir).unwrap_or(path).to_string_lossy(); - if let Err(e) = tera.add_raw_template(&template_name, &content) { - let rel = path.strip_prefix(dir).unwrap_or(path).display(); - errors.push(format!("Tera syntax error in {rel}: {e}")); - } - } - Err(e) => { - let rel = path.strip_prefix(dir).unwrap_or(path).display(); - warnings.push(format!("Could not read {rel}: {e}")); - } - } - } -} - -fn validate_tera_expression(expr: &str) -> std::result::Result<(), String> { - let template = format!("{{% if {expr} %}}ok{{% endif %}}"); - let mut tera = tera::Tera::default(); - tera.add_raw_template("__check__", &template) - .map_err(|e| e.to_string())?; - Ok(()) -} - -fn validate_tera_template(expr: &str) -> std::result::Result<(), String> { - let mut tera = tera::Tera::default(); - tera.add_raw_template("__check__", expr) - .map_err(|e| e.to_string())?; - Ok(()) -} diff --git a/src/cli.rs b/src/cli.rs index 3dae095..ab84986 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -49,18 +49,4 @@ pub enum Commands { /// List cached templates List, - - /// Validate a template directory - Check { - /// Path to the template to check (default: current directory) - #[arg(default_value = ".")] - path: String, - }, - - /// Check if a template is ready for distribution - Ready { - /// Path to the template to check (default: current directory) - #[arg(default_value = ".")] - path: String, - }, } diff --git a/src/commands/check.rs b/src/commands/check.rs deleted file mode 100644 index cc5b81d..0000000 --- a/src/commands/check.rs +++ /dev/null @@ -1,45 +0,0 @@ -use std::path::Path; - -use console::style; -use miette::Result; - -use diecut::check::check_template; - -pub fn run(path: String) -> Result<()> { - let template_dir = Path::new(&path); - - println!( - "{} {}", - style("Checking template at").bold(), - style(template_dir.display()).cyan() - ); - - let result = check_template(template_dir)?; - - println!(" Name: {}", result.template_name); - println!(" Variables: {}", result.variable_count); - - if !result.warnings.is_empty() { - println!("\n{}", style("Warnings:").yellow().bold()); - for w in &result.warnings { - println!(" {} {}", style("⚠").yellow(), w); - } - } - - if !result.errors.is_empty() { - println!("\n{}", style("Errors:").red().bold()); - for e in &result.errors { - println!(" {} {}", style("✗").red(), e); - } - println!( - "\n{} Template has {} error(s)", - style("✗").red().bold(), - result.errors.len() - ); - std::process::exit(1); - } else { - println!("\n{} Template is valid!", style("✓").green().bold()); - } - - Ok(()) -} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index b94123e..33661b9 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,4 +1,2 @@ -pub mod check; pub mod list; pub mod new; -pub mod ready; diff --git a/src/commands/ready.rs b/src/commands/ready.rs deleted file mode 100644 index 766b695..0000000 --- a/src/commands/ready.rs +++ /dev/null @@ -1,59 +0,0 @@ -use std::path::Path; - -use console::style; -use miette::Result; - -use diecut::ready::check_ready; - -pub fn run(path: String) -> Result<()> { - let template_dir = Path::new(&path); - - println!( - "{} {}", - style("Checking distribution readiness for").bold(), - style(template_dir.display()).cyan() - ); - - let result = check_ready(template_dir)?; - - println!(" Name: {}", result.check.template_name); - println!(" Variables: {}", result.check.variable_count); - - if !result.check.warnings.is_empty() { - println!("\n{}", style("Warnings:").yellow().bold()); - for w in &result.check.warnings { - println!(" {} {}", style("⚠").yellow(), w); - } - } - - if !result.check.errors.is_empty() { - println!("\n{}", style("Errors:").red().bold()); - for e in &result.check.errors { - println!(" {} {}", style("✗").red(), e); - } - } - - if !result.distribution_warnings.is_empty() { - println!("\n{}", style("Distribution:").yellow().bold()); - for w in &result.distribution_warnings { - println!(" {} {}", style("⚠").yellow(), w); - } - } - - if result.is_ready() { - println!( - "\n{} Template is ready for distribution!", - style("✓").green().bold() - ); - } else { - let total_issues = result.check.errors.len() + result.distribution_warnings.len(); - println!( - "\n{} Template has {} issue(s) to resolve before distribution", - style("✗").red().bold(), - total_issues - ); - std::process::exit(1); - } - - Ok(()) -} diff --git a/src/error.rs b/src/error.rs index 2710166..c7320f9 100644 --- a/src/error.rs +++ b/src/error.rs @@ -85,7 +85,7 @@ pub enum DicecutError { InvalidAbbreviation { input: String }, #[error("Hook '{hook}' failed: {message}")] - #[diagnostic(help("Check the Rhai script for errors"))] + #[diagnostic(help("Check the shell command in your hooks configuration"))] HookError { hook: String, message: String }, #[error("Cache metadata error: {context}")] diff --git a/src/lib.rs b/src/lib.rs index 2297f30..0106b7c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,11 +1,9 @@ pub mod adapter; pub mod answers; -pub mod check; pub mod config; pub mod error; pub mod hooks; pub mod prompt; -pub mod ready; pub mod render; pub mod template; diff --git a/src/main.rs b/src/main.rs index c587a9f..20cf462 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,7 +19,5 @@ fn main() -> miette::Result<()> { template, output, data, defaults, overwrite, no_hooks, dry_run, verbose, ), Commands::List => commands::list::run(), - Commands::Check { path } => commands::check::run(path), - Commands::Ready { path } => commands::ready::run(path), } } diff --git a/src/ready.rs b/src/ready.rs deleted file mode 100644 index 2a3118c..0000000 --- a/src/ready.rs +++ /dev/null @@ -1,47 +0,0 @@ -use std::path::Path; - -use crate::adapter::resolve_template; -use crate::check::{check_template, CheckResult}; -use crate::error::Result; - -pub struct ReadyResult { - pub check: CheckResult, - /// E.g. missing version, description, README. - pub distribution_warnings: Vec, -} - -impl ReadyResult { - pub fn is_ready(&self) -> bool { - self.check.errors.is_empty() && self.distribution_warnings.is_empty() - } -} - -/// Runs `check` validations plus distribution checks (version, description, README). -pub fn check_ready(template_dir: &Path) -> Result { - let check = check_template(template_dir)?; - let mut dist_warnings = Vec::new(); - - // Re-resolve to access config fields not exposed in CheckResult - let resolved = resolve_template(template_dir)?; - let config = &resolved.config; - - if config.template.version.is_none() { - dist_warnings.push("No 'version' specified in [template] section".to_string()); - } - - if config.template.description.is_none() { - dist_warnings.push("No 'description' specified in [template] section".to_string()); - } - - let has_readme = ["README.md", "README.txt", "README"] - .iter() - .any(|f| template_dir.join(f).exists()); - if !has_readme { - dist_warnings.push("No README file found in template root".to_string()); - } - - Ok(ReadyResult { - check, - distribution_warnings: dist_warnings, - }) -} From ff873d0189be91dd5c4fd3a12012fc4ba4887b5f Mon Sep 17 00:00:00 2001 From: rroskam Date: Sun, 15 Feb 2026 14:31:20 -0500 Subject: [PATCH 2/2] feat: add subpath support for multi-template repos Parse subpath from abbreviations like gh:user/repo/template-name, where segments after user/repo become the subpath within the cloned repo. Applies subpath to template_dir before resolution. This enables repos like diecut-templates to hold multiple templates in subdirectories, used as: diecut new gh:user/diecut-templates/python-pkg --- src/lib.rs | 16 +++- src/template/source.rs | 206 ++++++++++++++++++++++++++++++++++++----- 2 files changed, 196 insertions(+), 26 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 0106b7c..75cc4dd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -55,8 +55,22 @@ pub fn plan_generation(options: GenerateOptions) -> Result { commit_sha: None, }, ), - TemplateSource::Git { url, git_ref } => { + TemplateSource::Git { + url, + git_ref, + subpath, + } => { let (path, commit_sha) = get_or_clone(url, git_ref.as_deref())?; + let path = match subpath { + Some(sub) => { + let joined = path.join(sub); + if !joined.exists() { + return Err(DicecutError::TemplateDirectoryMissing { path: joined }); + } + joined + } + None => path, + }; ( path, SourceInfo { diff --git a/src/template/source.rs b/src/template/source.rs index b2859b0..750a854 100644 --- a/src/template/source.rs +++ b/src/template/source.rs @@ -10,6 +10,8 @@ pub enum TemplateSource { Git { url: String, git_ref: Option, + /// Subdirectory within the repo that contains the template. + subpath: Option, }, } @@ -46,7 +48,39 @@ fn build_github_url(rest: &str, protocol: &str) -> String { } } -fn expand_abbreviation(input: &str) -> Result { +/// Split an abbreviation remainder like "user/repo/some/path" into +/// the repo part ("user/repo") and an optional subpath ("some/path"). +fn split_repo_subpath(rest: &str) -> (&str, Option<&str>) { + let mut segments = 0; + let mut split_at = rest.len(); + for (i, c) in rest.char_indices() { + if c == '/' { + segments += 1; + if segments == 2 { + split_at = i; + break; + } + } + } + if split_at < rest.len() { + let subpath = &rest[split_at + 1..]; + if subpath.is_empty() { + (&rest[..split_at], None) + } else { + (&rest[..split_at], Some(subpath)) + } + } else { + (rest, None) + } +} + +#[derive(Debug)] +struct ExpandedSource { + url: String, + subpath: Option, +} + +fn expand_abbreviation(input: &str) -> Result { // Special case: GitHub abbreviation with protocol detection if let Some(rest) = input.strip_prefix("gh:") { if rest.is_empty() { @@ -54,8 +88,12 @@ fn expand_abbreviation(input: &str) -> Result { input: input.to_string(), }); } + let (repo, subpath) = split_repo_subpath(rest); let protocol = detect_github_protocol(); - return Ok(build_github_url(rest, &protocol)); + return Ok(ExpandedSource { + url: build_github_url(repo, &protocol), + subpath: subpath.map(String::from), + }); } // All other abbreviations use static expansion @@ -66,7 +104,11 @@ fn expand_abbreviation(input: &str) -> Result { input: input.to_string(), }); } - return Ok(format!("{base_url}{rest}{suffix}")); + let (repo, subpath) = split_repo_subpath(rest); + return Ok(ExpandedSource { + url: format!("{base_url}{repo}{suffix}"), + subpath: subpath.map(String::from), + }); } } Err(DicecutError::InvalidAbbreviation { @@ -77,7 +119,7 @@ fn expand_abbreviation(input: &str) -> Result { fn expand_user_abbreviation( input: &str, abbreviations: &HashMap, -) -> Option> { +) -> Option> { let (prefix, rest) = input.split_once(':')?; let url_template = abbreviations.get(prefix)?; @@ -88,7 +130,11 @@ fn expand_user_abbreviation( })); } - Some(Ok(url_template.replace("{}", rest))) + let (repo, subpath) = split_repo_subpath(rest); + Some(Ok(ExpandedSource { + url: url_template.replace("{}", repo), + subpath: subpath.map(String::from), + })) } fn is_abbreviation(input: &str) -> bool { @@ -123,19 +169,21 @@ pub fn resolve_source_full( ) -> Result { if let Some(abbrevs) = user_abbreviations { if let Some(result) = expand_user_abbreviation(template_arg, abbrevs) { - let url = result?; + let expanded = result?; return Ok(TemplateSource::Git { - url, + url: expanded.url, git_ref: git_ref.map(String::from), + subpath: expanded.subpath, }); } } if is_abbreviation(template_arg) { - let url = expand_abbreviation(template_arg)?; + let expanded = expand_abbreviation(template_arg)?; return Ok(TemplateSource::Git { - url, + url: expanded.url, git_ref: git_ref.map(String::from), + subpath: expanded.subpath, }); } @@ -143,6 +191,7 @@ pub fn resolve_source_full( return Ok(TemplateSource::Git { url: template_arg.to_string(), git_ref: git_ref.map(String::from), + subpath: None, }); } @@ -181,29 +230,35 @@ mod tests { #[test] fn expand_github_abbreviation() { - let url = expand_abbreviation("gh:user/repo").unwrap(); + let expanded = expand_abbreviation("gh:user/repo").unwrap(); assert!( - url == "https://github.com/user/repo.git" || url == "git@github.com:user/repo.git", - "unexpected URL: {url}" + expanded.url == "https://github.com/user/repo.git" + || expanded.url == "git@github.com:user/repo.git", + "unexpected URL: {}", + expanded.url ); + assert!(expanded.subpath.is_none()); } #[test] fn expand_gitlab_abbreviation() { - let url = expand_abbreviation("gl:org/project").unwrap(); - assert_eq!(url, "https://gitlab.com/org/project.git"); + let expanded = expand_abbreviation("gl:org/project").unwrap(); + assert_eq!(expanded.url, "https://gitlab.com/org/project.git"); + assert!(expanded.subpath.is_none()); } #[test] fn expand_bitbucket_abbreviation() { - let url = expand_abbreviation("bb:team/repo").unwrap(); - assert_eq!(url, "https://bitbucket.org/team/repo.git"); + let expanded = expand_abbreviation("bb:team/repo").unwrap(); + assert_eq!(expanded.url, "https://bitbucket.org/team/repo.git"); + assert!(expanded.subpath.is_none()); } #[test] fn expand_sourcehut_abbreviation() { - let url = expand_abbreviation("sr:~user/repo").unwrap(); - assert_eq!(url, "https://git.sr.ht/~user/repo"); + let expanded = expand_abbreviation("sr:~user/repo").unwrap(); + assert_eq!(expanded.url, "https://git.sr.ht/~user/repo"); + assert!(expanded.subpath.is_none()); } #[test] @@ -243,13 +298,18 @@ mod tests { fn resolve_abbreviation_to_git_source() { let source = resolve_source("gh:user/repo").unwrap(); match source { - TemplateSource::Git { url, git_ref } => { + TemplateSource::Git { + url, + git_ref, + subpath, + } => { assert!( url == "https://github.com/user/repo.git" || url == "git@github.com:user/repo.git", "unexpected URL: {url}" ); assert!(git_ref.is_none()); + assert!(subpath.is_none()); } _ => panic!("expected Git source"), } @@ -259,9 +319,14 @@ mod tests { fn resolve_explicit_https_to_git_source() { let source = resolve_source("https://example.com/repo.git").unwrap(); match source { - TemplateSource::Git { url, git_ref } => { + TemplateSource::Git { + url, + git_ref, + subpath, + } => { assert_eq!(url, "https://example.com/repo.git"); assert!(git_ref.is_none()); + assert!(subpath.is_none()); } _ => panic!("expected Git source"), } @@ -271,9 +336,14 @@ mod tests { fn resolve_git_ssh_to_git_source() { let source = resolve_source("git@github.com:user/repo.git").unwrap(); match source { - TemplateSource::Git { url, git_ref } => { + TemplateSource::Git { + url, + git_ref, + subpath, + } => { assert_eq!(url, "git@github.com:user/repo.git"); assert!(git_ref.is_none()); + assert!(subpath.is_none()); } _ => panic!("expected Git source"), } @@ -285,7 +355,7 @@ mod tests { fn resolve_with_ref_sets_git_ref() { let source = resolve_source_with_ref("gh:user/repo", Some("v1.0")).unwrap(); match source { - TemplateSource::Git { url, git_ref } => { + TemplateSource::Git { url, git_ref, .. } => { assert!( url == "https://github.com/user/repo.git" || url == "git@github.com:user/repo.git", @@ -301,7 +371,7 @@ mod tests { fn resolve_with_ref_none_leaves_ref_none() { let source = resolve_source_with_ref("gh:user/repo", None).unwrap(); match source { - TemplateSource::Git { url, git_ref } => { + TemplateSource::Git { url, git_ref, .. } => { assert!( url == "https://github.com/user/repo.git" || url == "git@github.com:user/repo.git", @@ -345,9 +415,14 @@ mod tests { ); let source = resolve_source_full("company:team/project", None, Some(&abbrevs)).unwrap(); match source { - TemplateSource::Git { url, git_ref } => { + TemplateSource::Git { + url, + git_ref, + subpath, + } => { assert_eq!(url, "https://git.company.com/team/project.git"); assert!(git_ref.is_none()); + assert!(subpath.is_none()); } _ => panic!("expected Git source"), } @@ -362,7 +437,7 @@ mod tests { ); let source = resolve_source_full("corp:myrepo", Some("v2.0"), Some(&abbrevs)).unwrap(); match source { - TemplateSource::Git { url, git_ref } => { + TemplateSource::Git { url, git_ref, .. } => { assert_eq!(url, "https://git.corp.com/myrepo.git"); assert_eq!(git_ref.as_deref(), Some("v2.0")); } @@ -431,4 +506,85 @@ mod tests { _ => panic!("expected Git source"), } } + + // ── Subpath parsing ──────────────────────────────────────────────── + + #[test] + fn split_repo_subpath_no_subpath() { + let (repo, sub) = split_repo_subpath("user/repo"); + assert_eq!(repo, "user/repo"); + assert!(sub.is_none()); + } + + #[test] + fn split_repo_subpath_single_segment() { + let (repo, sub) = split_repo_subpath("user/repo/template-a"); + assert_eq!(repo, "user/repo"); + assert_eq!(sub, Some("template-a")); + } + + #[test] + fn split_repo_subpath_nested() { + let (repo, sub) = split_repo_subpath("user/repo/templates/python"); + assert_eq!(repo, "user/repo"); + assert_eq!(sub, Some("templates/python")); + } + + #[test] + fn split_repo_subpath_trailing_slash() { + let (repo, sub) = split_repo_subpath("user/repo/"); + assert_eq!(repo, "user/repo"); + assert!(sub.is_none()); + } + + #[test] + fn resolve_abbreviation_with_subpath() { + let source = resolve_source("gh:user/repo/my-template").unwrap(); + match source { + TemplateSource::Git { + url, + subpath, + git_ref, + } => { + assert!( + url == "https://github.com/user/repo.git" + || url == "git@github.com:user/repo.git", + "unexpected URL: {url}" + ); + assert_eq!(subpath.as_deref(), Some("my-template")); + assert!(git_ref.is_none()); + } + _ => panic!("expected Git source"), + } + } + + #[test] + fn resolve_abbreviation_with_nested_subpath() { + let source = resolve_source("gl:org/repo/templates/python").unwrap(); + match source { + TemplateSource::Git { url, subpath, .. } => { + assert_eq!(url, "https://gitlab.com/org/repo.git"); + assert_eq!(subpath.as_deref(), Some("templates/python")); + } + _ => panic!("expected Git source"), + } + } + + #[test] + fn user_abbreviation_with_subpath() { + let mut abbrevs = HashMap::new(); + abbrevs.insert( + "company".to_string(), + "https://git.company.com/{}.git".to_string(), + ); + let source = + resolve_source_full("company:team/project/subdir", None, Some(&abbrevs)).unwrap(); + match source { + TemplateSource::Git { url, subpath, .. } => { + assert_eq!(url, "https://git.company.com/team/project.git"); + assert_eq!(subpath.as_deref(), Some("subdir")); + } + _ => panic!("expected Git source"), + } + } }