teravars 0.2.2

Tera templating + smart [vars] handling for self-rendering TOML configs
Documentation
[tasks.default]
alias = "check"

[tasks.check]
description = "Run editorconfig-check + fmt + clippy + test + lock-check (same as CI)"
dependencies = ["editorconfig-check", "fmt-check", "clippy", "test", "lock-check"]

[tasks.fmt-check]
description = "Check formatting"
command = "cargo"
args = ["fmt", "--all", "--", "--check"]

[tasks.editorconfig-check]
description = "Check tracked files against .editorconfig (skipped locally if the `editorconfig-checker` binary isn't on PATH — CI always enforces it via editorconfig-checker/action-editorconfig-checker). Install: https://github.com/editorconfig-checker/editorconfig-checker#installation"
script_runner = "@duckscript"
script = '''
ec_path = which editorconfig-checker
if is_empty ${ec_path}
    echo "editorconfig-checker not on PATH; skipping editorconfig-check locally (still enforced in CI). Install: https://github.com/editorconfig-checker/editorconfig-checker#installation"
    exit 0
end
exec --fail-on-error editorconfig-checker
'''

[tasks.fmt]
description = "Apply formatting"
command = "cargo"
args = ["fmt", "--all"]

[tasks.clippy]
description = "Run clippy with warnings as errors"
command = "cargo"
args = ["clippy", "--locked", "--all-targets", "--", "-D", "warnings"]

[tasks.clippy-all]
description = "clippy with --all-features"
command = "cargo"
args = ["clippy", "--locked", "--all-targets", "--all-features", "--", "-D", "warnings"]

[tasks.test]
description = "Run all tests (targets + doc)"
# `cargo test --all-targets` covers lib / bins / tests / benches /
# examples but EXCLUDES doc tests. Run --doc separately so the
# task name still matches its description ("all tests"). CI runs
# both jobs in parallel; locally we sequence them via dependencies.
dependencies = ["test-targets", "test-doc"]

[tasks.test-targets]
description = "cargo test --all-targets (lib / bins / tests / benches / examples — no doc)"
command = "cargo"
args = ["test", "--locked", "--all-targets"]

[tasks.test-doc]
description = "cargo test --doc (doc tests only — excluded from --all-targets). Skipped on bin-only packages, since `cargo test --doc` errors with `no library targets found` when there is no library to extract doc tests from. Drop the `condition` line once yukimemi/todoke#50 and yukimemi/rvpm#176 (lib + bin refactor) both land."
command = "cargo"
args = ["test", "--locked", "--doc"]
condition = { files_exist = ["${CARGO_MAKE_WORKING_DIRECTORY}/src/lib.rs"] }

[tasks.lock-check]
description = "Verify Cargo.lock is in sync with Cargo.toml"
command = "cargo"
args = ["check", "--locked", "--all-targets"]

[tasks.publish-dry]
description = "Dry-run crates.io publish"
command = "cargo"
args = ["publish", "--dry-run", "--allow-dirty"]

[tasks.hook-install]
description = "Install git pre-push hook that runs 'cargo make check'"
script_runner = "@duckscript"
script = '''
hook_dir = set ".git/hooks"
hook_file = set "${hook_dir}/pre-push"
mkdir ${hook_dir}
hook_content = set "#!/bin/sh\n# Installed by: cargo make hook-install\ncargo make check"
writefile ${hook_file} ${hook_content}
# chmod +x is needed on Unix; on Windows the exec bit is a no-op
# AND `chmod` may not be on PATH (it's only present when Git for
# Windows / busybox is wired into the shell). Skip silently if
# `chmod` isn't reachable so plain Windows shells don't fail this
# task with `program not found`.
chmod_path = which chmod
if not is_empty ${chmod_path}
    exec chmod +x ${hook_file}
else
    echo "chmod not on PATH; skipping exec-bit (no-op on Windows; on Unix this means the hook may not be executable)"
end
echo "Installed pre-push hook -> ${hook_file}"
'''

[tasks.apm-install]
description = "Install agent skills declared in apm.yml (Copilot / Claude / Gemini targets). Skipped silently when apm.yml or the apm CLI is absent."
script_runner = "@duckscript"
script = '''
exists = is_path_exists "apm.yml"
if not ${exists}
    echo "no apm.yml; skipping apm-install"
    exit 0
end
apm_path = which apm
if is_empty ${apm_path}
    echo "apm CLI not on PATH; skipping apm-install (install via aka.ms/apm-unix or your platform's package manager)"
    exit 0
end
exec apm install -t copilot,claude,gemini
'''

[tasks.apm-install-update]
description = "Refresh APM dependencies to upstream latest. Skipped when apm.yml/apm absent, or when apm.lock.yaml already records the current upstream renri HEAD (kicks in on every cargo make on-add after the first to keep worktree creation fast)."
script_runner = "@duckscript"
script = '''
exists = is_path_exists "apm.yml"
if not ${exists}
    echo "no apm.yml; skipping apm-install-update"
    exit 0
end
apm_path = which apm
if is_empty ${apm_path}
    echo "apm CLI not on PATH; skipping apm-install-update (install via aka.ms/apm-unix or your platform's package manager)"
    exit 0
end

# Fast-path: if apm.lock.yaml already records the current
# upstream HEAD of yukimemi/renri (the only APM dependency for
# projects using these templates), don't re-run `apm install --update` —
# git ls-remote is ~100ms, an actual reinstall is several
# seconds. This makes `cargo make on-add` cheap on every
# `renri add` after the first.
lock_exists = is_path_exists "apm.lock.yaml"
if ${lock_exists}
    ls_result = exec git ls-remote https://github.com/yukimemi/renri.git refs/heads/main
    ls_ok = eq ${ls_result.code} 0
    if ${ls_ok}
        upstream_line = trim ${ls_result.stdout}
        # `substring` errors out (failing the whole task) if the
        # input is shorter than the requested range, so length-check
        # *before* the call. Empty / short `upstream_line` happens
        # when `git ls-remote` succeeds-with-empty (deleted main ref,
        # weird CI mirror, etc.) — in that case we skip the fast-
        # path and fall through to a real `apm install --update`.
        line_len = length ${upstream_line}
        line_long_enough = greater_than ${line_len} 39
        if ${line_long_enough}
            upstream_sha = substring ${upstream_line} 0 40
            # Defense-in-depth: still re-check sha_len in case
            # duckscript's substring semantics ever change.
            # `contains "haystack" ""` returns true (empty needle is
            # always a substring), so a stray empty sha would also
            # incorrectly skip the install without this guard.
            sha_len = length ${upstream_sha}
            sha_valid = eq ${sha_len} 40
        else
            sha_valid = set false
        end
        if ${sha_valid}
            lock_body = readfile apm.lock.yaml
            already_synced = contains ${lock_body} ${upstream_sha}
            if ${already_synced}
                echo "apm.lock.yaml already at upstream renri ${upstream_sha}; skipping apm install --update"
                exit 0
            end
        end
    end
end

exec apm install --update -t copilot,claude,gemini
'''

[tasks.clippy-none]
description = "clippy with --no-default-features"
command = "cargo"
args = ["clippy", "--locked", "--all-targets", "--no-default-features", "--", "-D", "warnings"]

[tasks.setup]
description = "One-time setup for new contributors: pre-push hook + APM install"
dependencies = ["hook-install", "apm-install"]

[tasks.on-add]
description = "Wired to renri's [[hooks.post_create]] — runs in a freshly created worktree"
# `apm-install` (no `--update`) is idempotent against the existing
# `apm.lock.yaml` — fresh worktrees install the recorded skills
# without rewriting the lock. Upstream-refresh is delegated to
# pj-base's `.github/workflows/apm-bump.yml` (weekly cron + manual
# dispatch); same Renovate-equivalent pattern as `kata-apply.yml`.
# Avoids the per-`renri add` lock churn that `apm install --update`
# produced whenever upstream renri (or any other apm dep) had
# moved — see yukimemi/pj-base#8 for the companion change in
# `renri.toml.base`.
dependencies = ["apm-install", "vcs-fetch"]

[tasks.vcs-fetch]
description = "Fetch latest refs (jj when in a jj workspace, otherwise git)"
# Best-effort: a missing `jj` / `git` binary, an unreachable remote, etc.
# should log but not fail `cargo make on-add` and break `renri add`.
ignore_errors = true
script_runner = "@duckscript"
script = '''
# A non-colocated jj workspace has `.jj/` but no `.git/`, and `git fetch`
# fails there with "not a git repository". Prefer `jj git fetch`, which
# works in pure-jj and colocated repos. Fall back to `git fetch` for
# pure-git (no `.jj/`) worktrees.
jj_present = is_path_exists ".jj"
if ${jj_present}
    exec jj git fetch
else
    exec git fetch
end
'''

[tasks.test-all]
description = "test with --all-features"
command = "cargo"
args = ["test", "--locked", "--all-features"]

[tasks.test-none]
description = "test with --no-default-features"
command = "cargo"
args = ["test", "--locked", "--no-default-features"]