Skip to main content

Apply reusable local recipes across plain directories.

Project description

untaped-recipe

untaped-recipe is a standalone CLI for applying trusted local recipes across plain directories. It is built on the untaped SDK and deliberately does not clone repos, create branches, commit, push, or open PRs.

Install

uv tool install git+https://github.com/alexisbeaulieu97/untaped-recipe.git

Configure

Installed packs and backup bundles live under ~/.untaped/untaped-recipes by default.

untaped-recipe config set library_root ~/.untaped/untaped-recipes

The setting is stored in the shared untaped config under the recipe section. External hook requests time out after hook_timeout_seconds seconds, default 60; set it to 0 to disable the timeout for long-running trusted hooks. Hook environment startup (uv creating/syncing the pack env on first use) runs under its own hook_startup_timeout_seconds bound, default 300 (0 = unbounded), with a preparing hook environment for ... notice on stderr — a cold cache or lagging package index no longer counts against the hook timeout. Backup retention can be configured with backup_keep (newest N bundles) and backup_max_age_days; backup prune uses them when its flags are omitted.

Library Model

The library has one installable item type: packs.

  • installed pack projects under <library_root>/packs/<pack-id>/
  • install-source bookkeeping in <library_root>/packs.toml
  • backup bundles under <library_root>/backups/

Packs are uv projects. Public pack identity comes from [project].name in the top-level pyproject.toml; untaped-recipe-ansible installs as pack ansible:

[project]
name = "untaped-recipe-ansible"
version = "0.1.0"

[dependency-groups]
dev = ["untaped-recipe>=0.9"]

[tool.untaped_recipe]
requires_hook_api = ">=0.9,<1"

[tool.untaped_recipe.recipes]
"playbook-migration" = { path = "recipes/playbook-migration/recipe.yml" }

[tool.untaped_recipe.hooks]
"add_play_collections" = { module = "ansible_hooks.hooks.add_play_collections" }

Recipe YAML is behavior-only. It contains version, optional description, optional inputs, and steps; name: is rejected.

Single-file recipes are still supported by explicit path, for quick local use:

untaped-recipe apply ./recipe.yml ./service-a --yes

They are not installed as loose recipes/<name>.yml library items.

Authoring

Hooks are referenced from recipes by name. Recipes do not declare hook runtimes, and hook manifest rows do not declare kind; the exported function name is the contract. A hook module exports transform(), validate(), or both, and the recipe step type selects which function runs.

[tool.untaped_recipe.hooks]
"add_play_collections" = { module = "ansible_hooks.hooks.add_play_collections" }

Supported hook sources are:

  • the recipe's own pack
  • installed packs
  • built-ins such as yaml_edit, which are engine-owned and run in-process

Generated hooks use TYPE_CHECKING imports from untaped_recipe.hook_api for editor discovery through the dev-only untaped-recipe dependency. Pack projects must not depend on untaped-recipe at runtime; the installed CLI provides the worker and helper implementation. Runtime hook dependencies belong in [project].dependencies, and type-only authoring dependencies belong in [dependency-groups].dev because workers execute with uv run --locked --no-dev. Hook scaffolding refreshes uv.lock, so it needs access to PyPI or a configured uv source for untaped-recipe. The scaffolded dev dependency tracks the hook API floor for editor type discovery, not every CLI release.

If uv lock fails after scaffold files are written, new pack, new recipe, and new hook leave the pack, recipe, hook module, tests, and manifest rows in place and print a repairable error. Fix the package index or add a package-specific [tool.uv.sources] override, then run uv lock in the pack. For example, a lagging corporate mirror can route only untaped-recipe to an approved fallback index:

[tool.uv.sources]
untaped-recipe = { index = "approved-pypi" }

[[tool.uv.index]]
name = "approved-pypi"
url = "https://pypi.org/simple"
explicit = true

uv also provisions Python interpreters from GitHub on demand. On networks that block it, point UV_PYTHON_INSTALL_MIRROR at an approved mirror of python-build-standalone (or preinstall a matching interpreter) so pack environments can build at all.

Use --no-lock with new pack, new recipe, or new hook to skip the lock step entirely. The command exits successfully and prints a stderr note, but hooks cannot run until uv lock succeeds because workers use uv run --locked --no-dev.

untaped-recipe new pack ansible
untaped-recipe new recipe ansible/playbook-migration
untaped-recipe new hook ansible/add_play_collections
untaped-recipe add ./ansible --yes
untaped-recipe hook run ansible/add_play_collections --target ./service-a --file site.yml --diff

Apply

untaped-recipe apply add-config ./service-a ./service-b --var service=api
untaped-recipe apply ansible/playbook-migration ./service-a --yes
untaped-recipe apply ./pack-project ./service-a --recipe playbook-migration --yes
untaped-recipe apply ./recipe.yml ./service-a --yes
untaped-recipe apply add-config --stdin --yes --format json
untaped-recipe apply add-config --stdin --input-from service='{{ record.repo }}' --yes
untaped-recipe apply add-config ./service-a --dry-run
untaped-recipe apply add-config ./service-a --check
untaped-recipe apply add-config ./service-a --preview diff

apply plans every target first, prints a stderr preview, then asks for confirmation unless --yes is passed. Normal apply and --dry-run default to --preview table, which shows a file-level table with absolute paths, change kind, and line counts. --check defaults to summary-only preview output for CI; pass --preview table when you want the same file table in check mode. Use --preview diff for patch-compatible unified diffs with a/ and b/ relative paths, or --preview none for summary-only runs. --preview controls safety review detail; --quiet only mutes success chatter after the run. Backups are created by default before writing and can be restored later. Target writes are transactional: if a target cannot be written safely, that target is rolled back and reported as failed. Use --check for CI or compliance checks: it writes nothing, creates no backups, prompts for nothing, and exits non-zero when any target would change.

Recipes can list known candidate files explicitly for transform and remove steps. transform.files and remove.files are expanded into ordinary per-file steps, and transform can use optional: true to skip playbooks or config files that are absent in some targets. Missing optional transforms are reported as warnings in recipe.outcome rows. There is no globbing; recipes name the candidate paths they intend to touch.

Piped stdin accepts bare paths and untaped pipe records. Recipe resolves absolute record.target_path first, then falls back to record.path for generic path records. Records whose kind ends in .summary are informational and skipped as non-targets. Repo-grain records such as workspace.repo must provide target_path; older saved streams that only contain path plus repo are rejected instead of writing to the wrong directory.

Recipe inputs may be invocation-global or per-target. Input specs support description, sensitive, scope, and from in addition to type, default, and required. Omitted scope infers target when from is present and global otherwise. Per-target from values are sandboxed strict native Jinja strings evaluated only for scalar input derivation. They may combine literal text, string/number/boolean/null constants that Jinja parses without operators, and field access on target.path, target.name, target.parent_path, target.parent_name, or optional incoming pipe record. There are no ambient Jinja globals; control blocks, filters, tests, calls, operators, and collection literals are rejected, so negative numeric expressions like {{ -1 }} are not valid V1 sources. Missing, undefined, or null candidates fall through; false, 0, and empty strings are real values. Oversized or non-scalar derived values are rejected.

Use --input-from NAME=JINJA to override a per-target source, --var or --vars to provide fixed values, and --interactive to prompt for unresolved inputs. A fixed value and source override for the same input is rejected. scope: global rejects recipe from and --input-from, but accepts --var/--vars. Interactive prompts run before recipe defaults; an empty answer accepts the default when one exists. --interactive --check is rejected. With --stdin --interactive, target records still come from stdin and prompts use the controlling terminal. --stdin writes still require --yes unless --dry-run or --check is used.

Every recipe.outcome row includes resolved declared inputs. Inputs marked sensitive: true are redacted in rows, warnings/errors, and backup metadata; file-level previews and diffs are suppressed for targets with sensitive inputs. Real values still reach templates and hooks. Backup file entries record redacted per-target inputs and never store the full incoming pipe record.

Library Commands

untaped-recipe new pack <name> [--no-lock]
untaped-recipe new recipe <pack>/<name> [--no-lock]
untaped-recipe new hook <pack>/<name> [--no-lock]
untaped-recipe add <path|git-url> [--rev REV] [--name NAME] [--force]
untaped-recipe list [--packs|--hooks]
untaped-recipe show <pack|recipe-ref|hook-ref>
untaped-recipe check [pack|recipe-ref|path]
untaped-recipe test [pack|path|pack/recipe] [--update]
untaped-recipe remove <pack>
untaped-recipe edit <pack|recipe-ref|hook-ref>
untaped-recipe hook run <hook-ref>
untaped-recipe backup list|show|restore
untaped-recipe backup prune [--keep N] [--older-than DAYS]

add installs a pack from a local path or git URL and asks for confirmation after listing the recipes and hooks being installed. --name overrides the installed pack key; that key is the identity used by refs, output rows, ambiguity errors, check, and remove.

list shows recipes by default. list --packs shows installed packs and list --hooks shows hook refs. show renders structured pack, recipe, or hook detail. check is static preflight; without a ref it validates the whole library and packs.toml, and with a ref it validates one pack or recipe. test runs golden-fixture cases packs ship under tests/; --update regenerates goldens for an explicit pack or recipe. remove <pack> is destructive because library packs are editable in place; it requires confirmation or --yes. backup show and backup restore accept full ids, unambiguous prefixes, or latest; restore previews and confirms like apply, uses the same transactional write path and symlink confinement, and preserves the changed-since-backup hash guard unless --force is passed. Backups store text content and do not preserve file mode or mtime.

hook run is a no-write debug harness. Transform hooks require --file; by default the command reads --target/--file and writes exact transformed content to stdout with no added newline. Use --content TEXT, --content -, or --content-file PATH to supply fixture content without requiring the target file to exist. Use --diff to emit a unified diff instead of raw content. Validate hooks reject file/content options and emit a recipe.hook_run verdict record. Repeated --input KEY=VALUE and --arg KEY=VALUE values are YAML-parsed and override --inputs/--args YAML mapping files. Fixture context and hook diagnostics go to stderr; structured --format json|yaml|table|pipe output omits raw input and arg values. Use SDK --quiet when ad-hoc fixture values should not be echoed in a shared terminal.

See docs/packs.md, docs/recipes.md, and docs/hooks.md for pack, schema, and hook authoring details. See docs/migration-0.9.md for the 0.9.0 breaking changes.

Development

uv sync
uv run pre-commit run --all-files
uv run ruff check --fix
uv run ruff format
uv run mypy
uv run pytest
uv build

See AGENTS.md for architecture rules and product contracts.

Security

Please report suspected vulnerabilities privately. See SECURITY.md.

Contributing

See CONTRIBUTING.md and AGENTS.md for the local workflow, architecture rules, product contracts, and docs/release.md for the release workflow.

License

MIT. See LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

untaped_recipe-0.12.0.tar.gz (154.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

untaped_recipe-0.12.0-py3-none-any.whl (100.7 kB view details)

Uploaded Python 3

File details

Details for the file untaped_recipe-0.12.0.tar.gz.

File metadata

  • Download URL: untaped_recipe-0.12.0.tar.gz
  • Upload date:
  • Size: 154.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.19 {"installer":{"name":"uv","version":"0.11.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for untaped_recipe-0.12.0.tar.gz
Algorithm Hash digest
SHA256 bb9362744609672bd85aeacb519b3e5acc0ce51a669cfedba8622b9f10105eab
MD5 95a18cb84e45b01d01ec33c3c057b121
BLAKE2b-256 baad767eb850a4386b64dcff5df6901ce7524cba02af2b5f805d910db757d583

See more details on using hashes here.

File details

Details for the file untaped_recipe-0.12.0-py3-none-any.whl.

File metadata

  • Download URL: untaped_recipe-0.12.0-py3-none-any.whl
  • Upload date:
  • Size: 100.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.19 {"installer":{"name":"uv","version":"0.11.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for untaped_recipe-0.12.0-py3-none-any.whl
Algorithm Hash digest
SHA256 8b062bd7a5a3a299f4c20df5e4b765e3d1c08eab5ec5ffc5063d48c052cce233
MD5 4c9eb853f1fe5fb34759b7c308682e4f
BLAKE2b-256 8a9346e48eaf6d6f517029b185339f0a56929617235597a523d531ad6b5a03bd

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page