CLI Reference
Complete command and flag reference for the deft binary, derived from the
clap definitions in cli.rs and the dispatch logic in
main.rs.
deft [-v|--verbose]... [-q|--quiet] <COMMAND>
Global Constraints
Two global flags are declared on the top-level Cli struct with
global = true, meaning they are accepted before or after the subcommand:
#[arg(short, long, action = clap::ArgAction::Count, global = true)]
pub verbose: u8,
#[arg(short, long, global = true, conflicts_with = "verbose")]
pub quiet: bool,
-v/--verbose(repeatable counting flag). Usesclap::ArgAction::Count, soverboseis au8incremented once per occurrence:-v→1,-vv→2,-vvv→3, etc. Internally, however, every call site only ever checkscli.verbose > 0(seemain()in main.rs:let verbose = cli.verbose > 0;) — there is currently no behavioral distinction between-vand-vv; both enable the same single verbose mode (extra[engine]/[resolver]/[deft]diagnostic lines prefixed in dim gray). The counting arity exists in the parser today primarily for forward compatibility with finer-grained verbosity levels.-q/--quiet. A plain boolean. Declared withconflicts_with = "verbose"— clap will reject any invocation that mixes-qwith-v/--verboseat the argument-parsing stage, before deft’s own code ever runs. Quiet mode suppresses the green/cyan progress lines (Compiling,Linking,Locking,Updated,Created,Migrated,Syncing, etc.) that every command prints by default, but does not suppress hard errors (printed viaeprintln!to stderr regardless ofquiet) or the unconditionaldeft migrateunmapped-source warning (see migration.md).
Both flags are parsed once at the top of main() and threaded explicitly
through every command handler as (verbose: bool, quiet: bool) parameters —
there is no global/thread-local state.
Command Matrix
deft build
deft build [--release] [-o NAME] [-j N] [--manifest-path DIR]
[--features A,B,C] [--no-default-features]
| Flag | Mechanics |
|---|---|
--release | Boolean. Passed through to Compiler::new(..., release) and Engine::build_package(..., release). Two concrete effects: (1) Compiler::effective_opt unconditionally returns OptLevel::O3, ignoring whatever optimization string is set in [profile.c]/[profile.cpp] — release always means -O3, full stop, regardless of manifest config; (2) push_common appends -DNDEBUG and omits -g. Debug builds (release = false) do the opposite: honor the manifest’s optimization field via OptLevel::parse, and always append -g. |
-o, --output NAME | Option<String>. Overrides the artifact’s base filename (before the platform-specific extension is applied: .exe/bare on Unix for executables, .lib/lib*.a for libraries). Defaults to the package name. |
-j, --jobs N | Option<usize>. Resolved by the jobs() helper in main.rs: args.jobs.unwrap_or_else(default_jobs).max(1). This is the clamping: an explicit -j is floored to a minimum of 1 (so -j 0 cannot spawn zero workers), and an absent -j falls back to std::thread::available_parallelism(). Engine::new applies a second floor (jobs.max(1)) and compile_all further clamps the actual worker count to self.jobs.min(total) — never more threads spawned than there are translation units to compile. |
--manifest-path DIR | Option<PathBuf>. May point at a directory or directly at a deft.toml file (project_root strips the filename in the latter case). Defaults to the current working directory. Resolution fails fast with LayoutViolation if no deft.toml is found at the resolved root. |
--features A,B,C | Vec<String>, comma-delimited (value_delimiter = ','). Unioned with the manifest’s default feature set (unless suppressed) and transitively expanded — see manifest.md. |
--no-default-features | Boolean. Suppresses automatic inclusion of the [features] default set; explicitly-passed --features are still honored. |
Profile mapping. build_single loads manifest.profile.c /
manifest.profile.cpp (each Option<CProfile>/Option<CppProfile>,
defaulting via .unwrap_or_default() if the table is absent from
deft.toml) and constructs one Compiler for the whole package — the same
Compiler instance answers compile_unit for every translation unit,
dispatching internally to c_args/cpp_args per source file’s detected
language (see architecture.md).
Workspace builds. If manifest.is_workspace() (a non-empty
[workspace] members list), cmd_build delegates to build_workspace,
which builds every member directory in declaration order via build_single
and returns the last member’s BuildOutcome as the overall result —
there is no parallelism across workspace members, only within each member’s
own translation units.
Dependency build-before-root ordering. Resolved dependencies are always
compiled — as libraries, regardless of whether their own layout would
otherwise resolve to an executable (Layout { crate_kind: Crate::Library, ..dep_layout } forcibly overrides the kind) — before the root package, so
their archives and src//include/ headers exist as -I include paths by
the time the root package’s units are planned.
deft run
deft run [build flags...] [-- ARGS...]
pub struct RunArgs {
#[command(flatten)]
pub build: BuildArgs,
#[arg(last = true, value_name = "ARGS")]
pub bin_args: Vec<String>,
}
RunArgs flattens the entire BuildArgs struct (#[command(flatten)]), so
every deft build flag documented above is also a valid deft run flag with
identical semantics — deft run is implemented as “build, then exec” with no
separate flag surface.
- Validation against library crates. After
build_with_diagnosticsreturns aBuildOutcome,cmd_runchecksoutcome.crate_kind != Crate::Executableand, if the package resolved to aLibrary(i.e. its entry point wassrc/lib.cpp/src/lib.c), returnsDeftError::LayoutViolation("\deft run` requires an executable (src/main.cpp or src/main.c)")` after the build has already succeeded — a library still gets fully compiled and archived; only the “now execute it” step is rejected. - Verbatim argument forwarding.
#[arg(last = true)]is clap’s “greedy positional after--” marker: everything after a literal--token on the command line is captured intobin_argsuntouched — not reinterpreted as deft flags, not split/escaped/re-quoted. These are passed straight through to the child process viaCommand::new(&outcome.artifact).args(&args.bin_args). This is whydeft run --release -- --releasecorrectly applies--releaseto the build once and forwards the literal string--releaseas the binary’s own argv — clap stops parsing deft’s own flags at the first bare--. - The child’s exit status is propagated: a non-zero exit causes
std::process::exit(status.code().unwrap_or(1))from the deft process itself, so shell scripts checking$?afterdeft runsee the binary’s exit code, not deft’s.
deft init
deft init [PATH] [--name NAME] [--lib | --bin] [--c]
PATHdefaults to.(current directory); created withcreate_dir_allif absent, along withPATH/src.--namedefaults to the canonicalized directory’s file name (falling back to the literal string"my_project"if canonicalization fails, e.g. for a not-yet-existing relative path).Language/kind selection.
--liband--binare mutually exclusive (conflicts_with = "bin"on--lib);is_lib = args.lib && !args.binmeans the default (neither flag) is an executable.--cswitches the generated language from C++ (default) to C. The four combinations select one of four hardcoded template pairs:--lib--cEntry file Template constant no no src/main.cppCPP_MAIN(#include <iostream>, prints “Hello from deft!”)no yes src/main.cC_MAIN(#include <stdio.h>,printf)yes no src/lib.cppCPP_LIB(adeft_add(int, int)function)yes yes src/lib.cC_LIB(same, C-flavored comment style)Overwrite protection. Before writing,
cmd_initchecksentry_path.exists()and returnsDeftError::LayoutViolation("... already exists; refusing to overwrite")— init never clobbers an existing entry file. The manifest (deft.toml) and.gitignoreget the same treatment but via simple existence checks that silently skip writing rather than erroring (if !manifest_path.exists() { ... }), so re-runningdeft initin an already-initialized directory is a safe no-op for those two files as long as the entry source file itself is untouched.The generated manifest embeds a matching
[profile.c]or[profile.cpp]block (C_PROFILE/CPP_PROFILEconstants) so a freshly-init’d package builds immediately without further configuration.A
.gitignorecontaining/targetis written if absent.
deft doctor
deft doctor
Takes no package-specific arguments — it diagnoses the environment, not a
particular project. Runs exactly seven checks, every one of them inline in
doctor::run (doctor.rs):
clang --versionpresent (C compiler).clang++ --versionpresent (C++ compiler).ar --versionpresent (archiver — note this checks Unixarspecifically even on Windows, where the build path would actually tryllvm-ar/lib.exe;doctor’sarcheck is a baseline binutils probe).git --versionpresent (required forgh:dependency resolution).- A native fetch tool is present:
powershellon Windows ($PSVersionTable.PSVersion), elsecurl --versionfalling back towget --version. - The end-to-end compilation probe.
check_system_headerswrites a throwaway file to the OS temp directory, named uniquely per-process (deft-doctor-<pid>.c), containing exactly:then invokes#include <stdio.h> int main(void){return 0;}clang -c <probe>.c -o <probe>.oand checks the exit status. This catches failures that “is clang on PATH” alone cannot — a broken sysroot, a missing or misconfigured libc headers package, or a clang installation that can’t find its own resource directory. Both the probe source and the resulting object file are deleted (remove_file, best-effort) regardless of outcome. $DEFT_HOME(or$HOME/.deftif unset) is locatable. This check always reportsok: trueeven when the directory doesn’t exist yet — it only fails hard if neither$DEFT_HOMEnor$HOMEis set at all, since the directory itself is lazily created on first build/resolve.
OS-aware fix suggestions. Every failing check carries an optional
fix: Option<String> rendered under a fix: line in the report. Compiler and
archiver fixes branch on std::env::consts::OS:
fn install_hint_clang() -> String {
match std::env::consts::OS {
"macos" => "install LLVM: `brew install llvm`",
"windows" => "install LLVM: `winget install LLVM.LLVM`",
_ => "install clang: `sudo apt install clang` (or your distro's equivalent)",
}
}
install_hint_binutils follows the same three-way branch
(brew install binutils / “install LLVM, which ships llvm-ar, or MSYS2
binutils” / sudo apt install binutils).
doctor::run always returns Ok(()) — it is a report, not a gate: even
with every check failing, the process exit code stays 0 (the doctor module’s
own doc comment is explicit: “Returns Ok(()) even when checks fail —
doctor is a report, not a gate”). The pass/fail tally is purely a printed
summary line ("{passed} passed, {failed} failed.").
doctor is invoked two ways: explicitly via deft doctor, and automatically
(non-fatally — let _ = doctor::run(verbose);) by build_with_diagnostics
whenever a deft build or deft run invocation fails, right before deft
re-raises the original build error. See
architecture.md for why this is split
out from the build’s own hot path.
deft sync
deft sync
Refreshes the flat-text package index at ~/.deft/deft-libs (or
$DEFT_HOME/deft-libs) — the shorthand-to-URL mapping table used to resolve
gh:user/lib-style dependency keys that aren’t already covered by the
built-in gh: → https://github.com/<user>/<lib>.git heuristic.
cmd_sync constructs a Resolver and calls resolver.sync_index(quiet)
(resolver.rs). This is strictly an index refresh —
it loads no project manifest, resolves no dependency graph, and never reads
or writes a project’s deft.lock. The doc comments in both
cli.rs and resolver.rs call this out
explicitly to distinguish it from deft update.
Zero-dependency manifest indexing. The index’s source URL defaults to:
https://raw.githubusercontent.com/deft-cli/deft-libs/main/deft-libs
overridable via the DEFT_LIBS_URL environment variable (for self-hosted or
air-gapped registries). The fetch itself uses only host-native tools, chosen
by fetch_to_file:
- Windows (
cfg!(target_os = "windows")):fetch_with_powershellshells out topowershell -NoProfile -NonInteractive -Command "Invoke-WebRequest -Uri '<url>' -OutFile '<dest>'". - Unix:
fetch_with_curl_or_wgettriescurl --silent --show-error --fail --location --max-time 30 -o <dest> <url>first; if curl’sCommand::status()either errors (binary missing) or returns non-success, it falls back towget --quiet --timeout=30 -O <dest> <url>. Only if both fail does it surface aDeftError.
Atomicity. The fetch writes to a sibling deft-libs.tmp file first, then
fs::rename(&tmp, &dest) performs the visible swap — a fetch that dies
partway through (network drop, disk full) never corrupts the live index,
since the rename is the only operation that touches the real deft-libs
path.
deft update
deft update [PACKAGE] [--manifest-path DIR]
Re-resolves the current project’s dependency graph from scratch and
rewrites deft.lock — the inverse operation to deft sync (which never
touches deft.lock) and complementary to deft build (which, by design,
reads the lock and never silently re-resolves on its own — see
manifest.md).
- Full update (
PACKAGEomitted):cmd_updateloads the existing lock only to pass aspin = Noneregardless of whether it exists — every dependency is resolved fresh, fetching current HEAD SHAs for each tag viaresolver.resolve_all(&manifest, None). Every entry in the rewritten lock reflects a freshgit fetch/rev-parse HEAD, even dependencies whose declared version string didn’t change. - Scoped update (
PACKAGEgiven): the existing lock is passed aspinfor the initialresolve_allcall, so every dependency except the named target stays pinned to its previously-locked SHA. The named target is then explicitly re-resolved a second time withpin = None(resolver.resolve_all(&manifest, None)) and spliced into the result list, replacing the pinned entry.package_name()strips the shorthand down to its bare trailing path segment for the name comparison (e.g.gh:user/lib→lib), sodeft update libmatches regardless of which shorthand prefix was used. - Dependency cache state. Re-resolution does not necessarily mean
re-cloning:
Resolver::ensure_cachedreuses an existing checkout under~/.deft/cache/<name>-<tag>if it already contains a.gitdirectory, running onlygit fetch --depth 1 origin tag <tag>followed bygit checkout --quiet <tag>rather than a fresh clone. A fresh clone only happens when the cache directory is absent or doesn’t look like a git repo. - The rewritten lockfile is written via the same atomic
.tmp+renamepattern asdeft sync’s index (see manifest.md). - Non-quiet output prints one
Updated N dependenc{y,ies} in deft.lockline followed by onename vVERSION @ <10-char SHA prefix>line per resolved dependency (short_shatruncates to the first 10 characters, or the full string if shorter).