Numi is a blazingly fast CLI for generating code from Apple project resources. It turns asset catalogs, localization files, and file lists into generated accessors and helpers using built-in or custom templates.
cargo install numiThe installed binary is named numi.
- reads
.xcassetsand generates image and color accessors - reads
.stringsand.xcstringsand generates localization helpers - reads
filesinputs and generates file-oriented helpers - renders built-in templates or custom Minijinja templates
- supports workspace orchestration when a repo has multiple
numi.tomlfiles
Numi is built for deterministic generation workflows: check in the outputs you want, regenerate them locally, and verify them in CI with numi check.
Initialize a starter config in your project:
numi initGenerate code:
numi generateCheck whether committed generated files are up to date:
numi checkWorkspace orchestration is also available when a repo has multiple numi.toml files:
numi generate --workspace
numi check --workspaceNumi uses numi.toml as its config filename.
version = 1
[defaults]
access_level = "internal"
[defaults.bundle]
mode = "module"
[jobs.assets]
output = "Generated/Assets.swift"
[[jobs.assets.inputs]]
type = "xcassets"
path = "Resources/Assets.xcassets"
[jobs.assets.template.builtin]
language = "swift"
name = "swiftui-assets"
[jobs.l10n]
output = "Generated/L10n.swift"
[[jobs.l10n.inputs]]
type = "strings"
path = "Resources/Localization"
[jobs.l10n.template.builtin]
language = "swift"
name = "l10n"You can also point localization generation at .xcstrings:
[jobs.l10n]
output = "Generated/L10n.swift"
[[jobs.l10n.inputs]]
type = "xcstrings"
path = "Resources/Localization"
[jobs.l10n.template.builtin]
language = "swift"
name = "l10n"The starter config shipped with numi init lives in docs/examples/starter-numi.toml.
The same shape also works for Objective-C built-ins when you want an ObjC output:
[jobs.assets.template.builtin]
language = "objc"
name = "assets"numi generate
- discovers the nearest manifest unless
--configis passed - uses the nearest local
numi.tomlfirst - runs one config for
[jobs]manifests and the whole workspace for[workspace]manifests - generates outputs for all named jobs, or only selected jobs when
--jobis repeated - prints non-fatal warnings to stderr
- may reuse cached parser outputs when inputs are unchanged
numi check
- computes what
generatewould write without modifying files - exits
0when outputs are current - exits
2when outputs are stale - prints warnings to stderr without turning the run into a failure
numi dump-context
- prints the exact JSON context a job template receives
- only supports single-config (
[jobs]) manifests and rejects workspace manifests - is the fastest way to debug or author custom templates
numi config locate
- prints the resolved config path
numi config print
- prints the resolved config with defaults materialized
- only supports single-config (
[jobs]) manifests and rejects workspace manifests
Numi currently ships these built-in templates:
- Swift:
language = "swift",name = "swiftui-assets"language = "swift",name = "l10n"language = "swift",name = "files"
- Objective-C:
language = "objc",name = "assets"language = "objc",name = "l10n"language = "objc",name = "files"
Fonts are supported in the template context and in custom-template workflows, but the first public release does not ship a dedicated built-in Swift template for fonts.
.xcstringsplural and device-specific variations are skipped with warnings- the shipped
l10ntemplate currently emits simple no-argument accessors even when placeholder metadata is present in template context
Repos with more than one numi.toml can orchestrate them from a repo-level numi.toml:
version = 1
[workspace]
members = ["AppUI", "Core"]
[workspace.defaults.jobs.assets.template.builtin]
language = "objc"
[workspace.member_overrides.Core]
jobs = ["assets"]Then each member job can keep only the built-in name:
[jobs.assets.template.builtin]
name = "assets"Workspace members are directory roots, not config-file paths. From the repo root, plain numi generate and numi check use that nearest workspace numi.toml automatically. From inside a member directory, add --workspace when you want the ancestor workspace instead of the local member manifest. Workspace defaults can provide template.builtin.language, but each job still needs to pick its own built-in name.
Custom templates use Minijinja:
[jobs.l10n.template]
path = "Templates/l10n.jinja"Numi supports {% include %} from:
- the including template's local directory
- the config-root search path
If the same include path exists in both places, Numi errors instead of guessing.
Start custom-template work with:
numi dump-context --job l10nThe stable context contract is documented in docs/context-schema.md.
Useful local commands:
cargo fmt --all --check
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspacecrates.io release notes for the workspace live in docs/crates-io-release.md.
