Type-safe code from your
Apple
resources.
A blazingly fast Rust CLI that turns asset catalogs, localization files, fonts, and file lists into generated accessors — with generation hooks, workspace orchestration, and incremental caching. Swift and Objective-C built in, or write your own Minijinja templates.
- Written in
- Rust · 2024
- License
- MIT
- Runtime
- Zero
Three commands. Generated code you can trust.
- Step 01 1Scaffold$ numi init
Drops a starter numi.toml into your project root, preconfigured for common asset layouts.
- Step 02 2Generate$ numi generate
Parses your resources, renders templates, and writes output files — only when they changed.
- Step 03 3Verify$ numi check
Exits non-zero if committed files are stale. Wire it into CI and never merge drift again.
Everything you need to take generated code seriously.
Deterministic outputs
Byte-stable generation. Unchanged inputs never rewrite files — commit the output and trust it.
CI-safe with numi check
Exit 0 when fresh, exit 2 when stale. Drop it into any workflow and fail the build on drift.
Swift + Obj-C built-ins
Ship-ready templates for SwiftUI assets, localization, file catalogs, and fonts in both languages.
Minijinja templates
Bring your own templates with a stable context schema. Full access to jobs, inputs, and metadata.
Workspace orchestration
One manifest at the repo root, lean per-module configs that inherit shared defaults. Auto-detects the nearest ancestor workspace.
Cached incremental parses
Asset catalogs and strings files are parsed once, cached on disk, and skipped when untouched.
Generation hooks
Run formatters or scripts before and after generation. Per-job or workspace-wide, with full environment metadata.
Five input types
Asset catalogs, .strings, .xcstrings, file lists, and fonts. Parse once, render to any template.
Rich CLI tooling
Dump template context for debugging, locate and print resolved configs, and interactive terminal status.
One manifest in. Type-safe code out.
Declare jobs in numi.toml. Point at inputs, pick a built-in template, and commit the generated file. That's the whole loop.
version = 1
[defaults]
access_level = "internal"
[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 = "xcstrings"
path = "Resources/Localization"
[jobs.l10n.template.builtin]
language = "swift"
name = "l10n"// Generated by numi. DO NOT EDIT.
import SwiftUI
public enum Asset {
public enum Image {
public static let appIcon = ImageResource(name: "AppIcon", bundle: .module)
public static let heroBanner = ImageResource(name: "HeroBanner", bundle: .module)
public static let emptyState = ImageResource(name: "EmptyState", bundle: .module)
}
public enum Color {
public static let brandPrimary = ColorResource(name: "BrandPrimary", bundle: .module)
public static let surface = ColorResource(name: "Surface", bundle: .module)
public static let onSurface = ColorResource(name: "OnSurface", bundle: .module)
}
}// Generated by numi. DO NOT EDIT.
import Foundation
public enum L10n {
public enum Onboarding {
/// Welcome to Numi
public static let title = String(localized: "onboarding.title", bundle: .module)
/// Get started in seconds.
public static let subtitle = String(localized: "onboarding.subtitle", bundle: .module)
}
public enum Errors {
/// Something went wrong. Please try again.
public static let generic = String(localized: "errors.generic", bundle: .module)
public static func notFound(_ p0: String) -> String {
String(localized: "errors.not_found \(p0)", bundle: .module)
}
}
}// Generated by numi. DO NOT EDIT.
import Foundation
public enum Files {
public enum Fixtures {
public static let sampleJSON = Bundle.module.url(
forResource: "sample",
withExtension: "json"
)!
public static let seedDB = Bundle.module.url(
forResource: "seed",
withExtension: "sqlite"
)!
}
}Ship-ready. Or bring your own.
Every built-in is a Minijinja template backed by a stable context schema. Fork one, or write your own — the same data is available either way.
Strongly-typed accessors for image and color resources, ready to drop into SwiftUI views.
Generates localization enums with argument-preserving helpers for parameterized strings.
Emits Bundle.url helpers for arbitrary resource files — fonts, fixtures, seed data, anything.
Objective-C asset accessors for image and color resources with UIKit-friendly naming.
One repo. Many modules. Shared defaults.
A root workspace manifest orchestrates member configs, shares defaults for access levels, bundle modes, template paths, and hooks. Members stay lean — they inherit what they don't override.
version = 1
[workspace]
members = ["AppUI", "Core"]
[workspace.defaults]
access_level = "internal"
[workspace.defaults.bundle]
mode = "module"
[workspace.defaults.hooks.post_generate]
command = ["swiftformat"]
[workspace.defaults.jobs.assets.template.builtin]
language = "swift"version = 1
[jobs.assets]
output = "Generated/Assets.swift"
[[jobs.assets.inputs]]
type = "xcassets"
path = "Resources/Assets.xcassets"
[jobs.assets.template.builtin]
name = "swiftui-assets"Auto-detection
Run numi generate from any member directory and it auto-prefers the nearest ancestor workspace. No flags needed.
Generation hooks
Run formatters or scripts before and after generation. Hooks receive NUMI_JOB_NAME, NUMI_OUTPUT_PATH, and NUMI_WRITE_OUTCOME via env vars.
Inherited defaults
Workspace defaults cascade into members — access levels, bundle modes, template paths, and hooks. Job-level config overrides inherited values for that phase.
Every command you need.
Generate, verify, debug, and inspect — all from the terminal. Run numi --help for the full reference.
Generate outputs for one config or workspace
Check whether generated outputs are up to date
Write a starter numi.toml in the current directory
Print the resolved config path
Print the resolved manifest with defaults materialized
Print the template context for one job
Gate drift at the pull request.
numi check exits 0 when committed files are up to date and 2 when they're stale. Drop it into any workflow and your CI will fail the build before reviewers do.
- Deterministic output — no timestamps, no ordering noise.
- Workspace mode checks every manifest in one pass.
- No-op writes: unchanged files never get rewritten.
name: verify
on: [push, pull_request]
jobs:
numi-check:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- run: cargo install numi
- name: Verify generated files are up to date
run: numi check --workspaceGenerate once. Trust forever.
numi is MIT licensed, on Homebrew and crates.io. One command to install, one to try.