RFC 015: hatch-like tooling (project lifecycle CLI)¶
- Status: Implemented
- Created: 2025-12-23
- Author(s): Danny Meijer (@dannymeijer)
- Related:
- RFC 013 (Rust crate dependencies)
- RFC 020 (Cargo offline/locked policy)
- Issue: https://github.com/dannys-code-corner/incan/issues/73
- RFC PR: https://github.com/dannys-code-corner/incan/pull/400
- Written against: v0.1
- Shipped in: v0.3
Summary¶
Introduce a first-class, batteries-included project lifecycle CLI — similar in spirit to Python’s Hatch — for:
- Versioning:
incan version <major|minor|patch|alpha|beta|rc|dev>(with optional--dry-run) - Project scaffolding:
incan init(in-place) andincan new <name>(new directory) - Environments:
incan env ...for repeatable, named command execution (CI-friendly) without implicit “magic” - (future) Matrix testing (tox/nox-style): a follow-up RFC may add a matrix/env runner once RFC 019 stabilizes
- Additional “hatch-like” ergonomics where it fits Incan’s workflow (format/lint/release/build/publish).
This RFC defines the CLI surface, the project metadata format, and the implementation boundaries so we don’t bake policy into ad-hoc scripts.
Motivation¶
Incan is a compiler + runtime ecosystem, but day-to-day developer experience is heavily shaped by tooling:
- Starting a new project should be one command.
- Bumping versions should be correct and consistent across project metadata, derived artifacts, and any package metadata.
- Running tests should support repeatable environments and matrix execution, without forcing users to learn Cargo internals.
- Release workflows should be scriptable and standard across projects.
Python’s Hatch demonstrates that a single tool can cover the project lifecycle. This RFC adapts the useful parts to Incan.
Goals¶
- Provide an ergonomic, consistent, and scriptable CLI for common workflows:
init,new,version,test,env- (future)
fmt,lint,build,publish
- Define a single source of truth for project metadata (name, version, toolchain constraints, entrypoints, dependencies).
- Keep builds deterministic and reproducible (align with RFC 013 + RFC 020).
- Avoid “magic”: scaffolded project files are explicit and readable.
Non-Goals¶
- Implement a public package registry client (publish/install) in this RFC (can be a follow-up RFC).
- Replace Cargo for Rust-level dependency resolution (we can orchestrate Cargo, not reinvent it).
- Provide virtualenv-style isolation identical to Python (we’ll use explicit env configs and reproducible commands instead).
Terminology¶
- Project: An Incan repository containing Incan sources and metadata.
- Environment: A named configuration overlay for repeatable command execution (
cwd,env-vars, scripts, dependency overlays). - Matrix: Running an environment set across multiple dimensions (e.g., debug/release, features on/off).
Project Metadata¶
Add incan.toml at repo root (similar to pyproject.toml), as the canonical metadata source.
Minimal example (bin-style project)¶
[project]
name = "hello_incan"
version = "0.1.0"
# Entry points for project-aware execution (future: `incan run <script>`)
[project.scripts]
main = "src/main.incn"
[dependencies]
# Cargo/Rust crate dependencies for `rust::...` (RFC 013)
rand = "0.8"
serde = { version = "1.0", features = ["derive"] }
Notes:
[tool.incan]may contain additional tool-specific configuration (e.g., formatter settings, test timeouts). These are defined by their respective RFCs (e.g., RFC 019 for test configuration) and are not specified here.versionis SemVer-compatible with pre-release tags.- Rust dependencies integrate with RFC 013 rules.
incan.tomlis the project metadata and is intended to be edited.- Generated build artifacts under
target/are readable for debugging, but are not intended for manual editing (RFC 020).
[project] schema (normative)¶
The [project] table is the canonical, toolchain-owned metadata for an Incan project.
Required keys:
name: str: the project name.version: str: the project version (SemVer; pre-release tags allowed).
Optional keys (all str unless noted):
description: short human-readable description.authors: List[str]: list of author strings (recommended format:"Name <email@example.com>").maintainers: List[str]: list of maintainer strings (same format asauthors).license: SPDX license identifier or SPDX expression.license-files: List[str]: paths to license files (relative to project root; future-facing).readme: path to a readme file (relative to project root).homepage: project homepage URL.repository: source repository URL.documentation: documentation URL.issues: issue tracker URL.keywords: List[str]: keywords/tags (used for search/discovery; future-facing).classifiers: List[str]: trove-like classifiers (future-facing; useful for packaging/indexes).requires-incan: SemVer requirement for the minimum supported Incan toolchain version.private: bool: if true, the project must not be publishable (future: enforced byincan publish).
Validation rules:
namemust be non-empty and should be stable over the project’s lifetime.- Recommended (non-normative) name pattern:
^[a-zA-Z][a-zA-Z0-9_-]*$.
- Recommended (non-normative) name pattern:
versionmust be SemVer-compatible (including pre-release tags like-alpha.1).- Paths like
readmemust be relative to project root (unless absolute); if present they must not escape the project root.- The same rule applies to
license-filesand[project.scripts]entrypoint paths.
- The same rule applies to
Unknown keys:
- Unknown keys under
[project]should produce a warning (to catch typos) and are validated byincan check-config(future).
Full example (metadata-rich):
[project]
name = "my_app"
version = "0.1.0-alpha.1"
description = "An example Incan application"
authors = ["Arthur Dent <arthur.dent@example.com>", "Tricia McMillan <trillian@example.com>"]
maintainers = ["Build Team <build-team@example.com>"]
license = "MIT"
readme = "README.md"
homepage = "https://example.com/my_app"
repository = "https://github.com/example/my_app"
documentation = "https://docs.example.com/my_app"
issues = "https://github.com/example/my_app/issues"
keywords = ["incan", "cli", "example"]
# Minimum Incan version required to build/test this project
requires-incan = ">=0.2.0"
[project.scripts]
main = "src/main.incn"
[dependencies]
reqwest = "0.12"
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
# [tool.incan] may include formatter, test, or other tool-specific settings (see respective RFCs)
[project.scripts] schema (normative)¶
[project.scripts] maps script names to Incan entrypoint files (paths relative to project root).
- Values are
strpaths to.incnfiles. - A script name should be a simple identifier (recommended snake_case).
incan newmust create amainscript by default:main = "src/main.incn".
This RFC now defines project-aware incan run behavior for the default main script. Bare incan run may resolve [project.scripts].main when no file path is provided.
Note: [project.scripts] maps script names to .incn entrypoint paths. This is distinct from
[tool.incan.envs.<name>.scripts] (defined below), which maps script names to shell command argv lists for env execution.
Project root discovery (normative)¶
Most incan subcommands operate on a project.
Project root resolution:
- Starting from the current working directory, walk upward to find the nearest directory containing
incan.toml. - The first
incan.tomlfound determines the project root. - If no
incan.tomlis found, the command must fail with a clear diagnostic suggestingincan initorincan new.
Monorepos / nested projects:
- Nested
incan.tomlfiles are allowed; the nearest one wins. - A future extension may add explicit workspace support; this RFC’s behavior is intentionally simple and deterministic.
Override:
- Commands may accept
--project <path>to explicitly target a project root (path to a directory containingincan.toml).
When incan.toml is required (normative)¶
incan.toml is mandatory for project-aware commands:
incan test,incan version,incan env ...
incan.toml is not required for single-file execution:
incan run <file>andincan run -c "<code>"work without a project context.- In this mode, project-level features (dependencies, envs, versioning) are not available.
- If a single-file script needs Rust dependencies, it must use inline annotations (RFC 013) or be part of a project.
incan build <file> may operate in either mode:
- In project mode, project-level dependencies and strict lock semantics apply (RFC 013/020).
- In single-file mode, dependency configuration is limited to inline annotations and known-good defaults (RFC 013),
and strict flags operate on the generated project’s
Cargo.lock(RFC 020).
CLI Design¶
incan new <name>¶
Create a new directory containing a minimal Incan project scaffold:
incan.tomlsrc/main.incn(hello world)README.md.gitignore
The generated incan.toml must include:
[project]withnameandversion[project.scripts]withmain = "src/main.incn"
Reproducibility (normative):
incan newcreates a project withincan.tomlat the project root.- On the first
incan buildorincan test, the toolchain generatesincan.lockat the project root (per RFC 013). - Projects are recommended to commit
incan.lockfor reproducible builds (especially in CI).
Behavior:
- By default,
incan newis interactive: it prompts for project metadata (description, author, license, etc.). - Use
-y/--yesto skip prompts and use defaults (non-interactive mode for scripting/CI). - In non-interactive mode,
incan newrequires eitherNAMEor--dir <path>.
Flags:
- binary scaffolding is the default; there is no explicit
--binflag today --dir <path>(default:./<name>)--description <text>--author <author>(recommended format:"Name <email@example.com>")--license <license>(SPDX identifier or expression)--force(overwrite existing directory)-y/--yes(non-interactive; use defaults without prompting)
Note: --lib is intentionally deferred until there is a packaging/distribution story for Incan libraries.
incan init¶
Initialize incan.toml (and src/main.incn) in the current directory.
Behavior:
- By default,
incan initis interactive: it prompts for project metadata. - Use
-y/--yesto skip prompts and use defaults (non-interactive mode for scripting/CI).
Flags:
--name <name>--version <version>(default:0.1.0)--description <text>--author <author>(recommended format:"Name <email@example.com>")--license <license>(SPDX identifier or expression)--force(overwrite existing metadata)--detect(preserve an existingsrc/main.incnand reuse directory-derived defaults where possible)-y/--yes(non-interactive; use defaults without prompting)
incan version <bump>¶
Update the project version in incan.toml and any derived files that must match.
Scope (normative):
incan versionupdates the project version only.- Updating the Incan compiler/toolchain crate versions is a maintainer workflow and out of scope for this RFC.
Bumps:
major,minor,patchalpha,beta,rc,dev
Rules:
major/minor/patchoperate on the release core and clear pre-release unless--keep-prerelease.alpha/beta/rc/dev:- If no prerelease exists, append
-<tag>.1 - If same prerelease exists, increment numeric suffix
- If different prerelease exists, switch tag and reset to
.1
- If no prerelease exists, append
Flags:
--dry-run--set <version>(explicit override)--keep-prerelease-m/--message <msg>(for future integration with changelog/commit tooling)
Output should print:
- old version
- new version
- modified files
incan test¶
Default test runner entrypoint.
Behavior:
incan testruns the Incan test runner as specified by RFC 019 (project-neutral behavior).- Cargo policy flags (
--offline/--locked/--frozen) must be propagated consistently to any Cargo subprocesses, as per RFC 020.
This RFC intentionally does not define repo-maintainer workflows for the Incan compiler repository (e.g. “run all workspace Rust tests”); those are out of scope for user-facing tooling semantics.
Flags:
- The test runner’s flags and semantics are specified by RFC 019.
incan env¶
incan env provides a small “task/env runner” layer for repeatable commands, without changing the semantics of core
commands like incan test.
Core command stability (normative):
- Core commands like
incan testare not configurable viaincan.toml. Configuration is applied only when the user explicitly usesincan env run ...orincan env show ....
Core shape:
incan env list [--format text|json](list configured envs; outputs env names, one per line in text mode)incan env show <env> [--format text|json](show the fully-resolved env after inheritance/merging)incan env run <env> <script> [--dry-run] [-- <args...>](run a configured script in an env)
Configuration (normative):
[tool.incan.envs.default]
# The `default` env is included by other envs unless they set `detached = true`.
env-vars = { INCAN_NO_BANNER = "1" }
[tool.incan.envs.default.scripts]
test = ["incan", "test"]
[tool.incan.envs.unit]
# `default` is included implicitly for `unit` (unless `detached = true` is set).
env-vars = { INCAN_FANCY_ERRORS = "1" }
[tool.incan.envs.unit.scripts]
test = ["incan", "test"]
[tool.incan.envs.docs]
# Demonstrates env inheritance: `docs` includes `default` and also extends `unit`.
extends = ["unit"]
cwd = "workspaces/docs-site"
[tool.incan.envs.docs.scripts]
docs_build = ["python3", "-m", "mkdocs", "build", "-q"]
Normative behavior:
incan env listmust output all configured env names. Intextmode, one env name per line. Injsonmode, a JSON array of env names.incan env show <env>must resolve env inheritance and merging using the same rules asincan env runand then print:- resolved overlay chain (base → default? → extends… → env)
- resolved
cwd - resolved
env-vars - resolved scripts (and the final argv for each script)
- resolved dependency overlays (base + env additions/overrides)
--formatcontrols output format; if omitted,textis used.incan env run ...executes the configured script without any further env selection/indirection. In particular, invokingincan testinside an env script must run the test runner directly and must not “re-enter” env resolution.- Implementations must prevent accidental recursive self-invocation (e.g. an env script calling
incan env run ...in a way that would re-resolve the same env). If recursion is detected, the command must fail with a clear diagnostic. --separatesincan env runarguments from additional user arguments passed through to the underlying command.- There are no implicit lifecycle hooks (e.g. no automatic
pre*/post*script execution). Only the explicitly-invoked<script>is run. --dry-runmust print the resolved command (cwd,env-vars, argv) and exit successfully without executing it.
Example (--dry-run output):
$ incan env run unit test --dry-run -- -k "addition"
env: unit
cwd: /home/user/my_project
env-vars:
INCAN_NO_BANNER=1
INCAN_FANCY_ERRORS=1
command: incan test -k addition
Note:
env-varsshows the merged result after inheritance —INCAN_NO_BANNERcomes fromdefault,INCAN_FANCY_ERRORScomes fromunit.
Example:
# Run the "test" script in the "unit" env, passing "-k addition" to `incan test`
incan env run unit test -- -k "addition"
env (Environment) context:
- Environments may define:
extends: an optional list of other env names to include before this env (non-circular)detached: iftrue, do not include thedefaultenv (defaults tofalse)cwd: working directory to run scripts from (relative to project root unless absolute)env-vars: environment variables injected into the process environmentscripts: a mapping of script name to argv (List[str])- additional Rust dependencies to be merged into the project dependency set, using RFC 013 schema:
[tool.incan.envs.<name>.dependencies](merged into[dependencies])[tool.incan.envs.<name>.dev-dependencies](merged into[dev-dependencies])
Environment inheritance (normative; Hatch-like):
- There is a special env named
default. If it exists, it is included automatically for every other env unlessdetached = trueis set for that env. - An env may additionally declare
extends = ["env_a", "env_b", ...]. These envs are included (in order) before the env itself. - Duplicate inclusion is an error: if an env would appear more than once in the resolved overlay chain,
incan env show/runmust fail with a clear diagnostic. (Rationale: duplicates usually indicate a misconfigured graph and can create surprising override behavior.) - Cycles are forbidden. If inheritance is circular,
incan env show/runmust fail with a clear diagnostic. - Inheritance is a configuration overlay mechanism, not isolation: it does not create virtualenv-style sandboxes.
- All overlays are applied deterministically in this order:
project base →
default(if included) → extended envs (in order) → target env. - Merge behavior for common env fields:
- scripts: merged by name; later overlays override earlier ones on conflicts
- env-vars: merged by key; later overlays override earlier ones on conflicts (unsetting is out of scope)
- cwd: the last overlay that defines
cwdwins
Dependency merge semantics:
- Dependencies are additive (no removals).
- If the same crate key is specified in both base and env dependencies at any point in the chain:
- Version/source: the env entry replaces the base entry.
- Features: the env entry's features are unioned with the base entry's features.
- It is an error for an env to define both canonical and alias dependency tables (same rule as RFC 013), applied within the env scope.
- Envs cannot remove base dependencies; they can only add or override.
Additional Commands (future; non-normative)¶
These exist today in Makefiles across many repos, and this RFC leans toward CLI-native equivalents so projects do not need Make as a dependency.
However, they are intentionally deferred: these commands should be specified in follow-up RFCs once the core semantics of
incan test (RFC 019) and policy propagation (RFC 020) are settled.
incan fmt/incan fmt --checkincan lint(clippy-like checks for compiler + emitted code)incan smoke-test(build + tests + examples + benchmark smoke-check, mirroring current repo conventions)incan doctor(environment diagnostics: toolchain version, cargo, PATH, permissions)incan check-config(validateincan.tomlfor correctness and conflicts; may be folded intoincan doctor)- Example validations: required keys in
[project], path safety, env inheritance cycles, unknown keys/typos
- Example validations: required keys in
Extensibility (future; non-normative):
- Cargo-style third-party subcommands may be supported (similar to
cargo-foo→cargo foo): ifincan <cmd>is not built-in, the CLI may attempt to executeincan-<cmd>fromPATH.
Layers affected¶
- CLI surface —
incan new,incan init,incan version, andincan envare new top-level commands introduced by this RFC. Existing project-aware commands (build,run,test,lock) must consultincan.tomland derive project metadata from it. - Project manifest model —
incan.tomlmust be parsed, validated against the[project],[project.scripts], and[tool.incan.envs.*]schemas, and diagnosed precisely when keys are missing or invalid. - Project root discovery — commands must walk upward from the current directory to locate
incan.toml, with a--project <path>override, and all project-aware commands must agree on the resolved root. - Project generation — generated project files such as
Cargo.tomland entrypoint wiring must derive fromincan.tomlmetadata rather than hardcoded defaults. - Documentation — a project configuration reference and a "your first Incan project" guide are expected deliverables of this RFC.
Implementation Plan¶
Phase 1: Manifest model and project scaffolding¶
- Extend
incan.tomlparsing and writing so[project],[project.scripts], and[tool.incan.envs.*]are accepted and validated as project lifecycle metadata. - Keep project root discovery deterministic and reuse it from lifecycle commands instead of adding command-local search behavior.
- Implement interactive and non-interactive
incan init/incan newpaths that create explicit, readable starter files.
Phase 2: Version management¶
- Add SemVer-aware project version bumping for
major,minor,patch,alpha,beta,rc, anddev. - Support explicit version setting, dry-run output, and release-bump prerelease handling.
- Ensure version updates modify
incan.tomlas the project metadata source of truth.
Phase 3: Environment runner¶
- Parse and resolve
[tool.incan.envs.*]with deterministic default-env inclusion, ordered extension, duplicate detection, cycle diagnostics, and overlay merging. - Implement
incan env list,incan env show, andincan env run, including dry-run output and argument passthrough. - Prevent accidental recursive env invocations with a clear diagnostic.
Phase 4: Tests and documentation¶
- Add targeted unit and integration coverage for scaffolding, manifest validation, version bumping, env resolution, and env command execution.
- Update authored user-facing docs for first-project setup, project configuration, and lifecycle workflows.
- Add release notes for the RFC 015 user-facing CLI surface.
Implementation log¶
Spec / design¶
- Review RFC lifecycle state and confirm implementation is actively being picked up.
- Keep lifecycle command semantics aligned with RFC 013 dependency metadata and RFC 020 lock policy boundaries.
Manifest / project model¶
- Accept and validate RFC 015
[project]fields and[project.scripts]. - Accept and validate
[tool.incan.envs.*]configuration without rejecting unrelated future[tool.incan]keys unnecessarily. - Reuse project root discovery for lifecycle commands and project-aware diagnostics.
CLI / scaffolding¶
- Implement
incan new [name]with interactive metadata prompts and explicit starter files. - Update
incan initflags and behavior for non-interactive lifecycle setup. - Add CLI parsing and command wiring for lifecycle subcommands.
Version management¶
- Implement SemVer bump rules for release and prerelease bumps.
- Implement
--set,--dry-run, and--keep-prerelease. - Persist project version updates to
incan.tomland report modified files.
Environment runner¶
- Implement env listing and resolved env display.
- Implement env inheritance, merging, duplicate inclusion, and cycle diagnostics.
- Implement env script execution, dry-run output, argument passthrough, and recursion protection.
Tests¶
- Add unit coverage for manifest/schema validation and version/env resolution.
- Add integration coverage for
incan new,incan version, andincan env. - Run the repository-level pre-commit gate.
Docs¶
- Update first-project and lifecycle docs.
- Add project configuration reference coverage.
- Add release notes entry for RFC 015 / issue 73.
Implementation architecture¶
(Non-normative.) A practical rollout has four broad pieces:
- Metadata and scaffolding: support
incan.tomlparsing and validation, project-root discovery, and theincan new/incan initscaffolding path. - Version management: support SemVer-aware version bumping and explicit version setting against the project metadata source of truth.
- Environment runner: support
incan env list,show, andrun, including inheritance, deterministic overlay rules, dry-run output, and recursion or cycle diagnostics. - Documentation and polish: provide guide-level docs and examples that make the project lifecycle workflow discoverable for ordinary Incan users rather than repo maintainers only.
Implementation sequencing is not part of the public contract. The design claim is the project-lifecycle CLI surface and incan.toml model defined by this RFC, not any one internal rollout order.
Alternatives considered¶
- Rely solely on Makefile targets: simple but inconsistent across repos, hard to compose and introspect; also adds an extra tool dependency we don’t need.
- Embed everything in Cargo: good for Rust, but Incan’s source-of-truth isn’t Cargo.toml; also doesn’t cover project scaffolding or Incan-centric metadata.
- Adopt an existing tool (justfile, cargo-make): helps execution but doesn’t solve metadata/version semantics.
Design Decisions¶
- Development versions support both
-dev.Nand-dev+<sha>forms. -dev.Nis used for sequential dev releases such as0.2.0-dev.1.-dev+<sha>is used for CI-oriented build metadata where commit traceability matters.