# nils-common
`nils-common` is the workspace shared helper crate for cross-CLI primitives.
Primary constraint: shared helpers must preserve behavioral parity for each consuming CLI. Moving logic into this crate must not change user-facing output text, warnings, color behavior, or exit-code contracts.
## Shared helper policy
### What belongs in `nils-common`
- Reusable helper logic used by multiple CLI crates.
- Domain-neutral primitives (process/env/shell/git/clipboard/fs internals).
- APIs that return structured results and let callers own final UX text.
- Behavior that can be covered by deterministic unit tests.
### What stays crate-local
- User-facing warning/error text (including emoji/prefix wording).
- Exit-code mapping and command-level failure policy.
- CLI-specific command composition and UX defaults.
- Product/business/domain flows that only make sense in one crate.
## Modules and purpose
- `env`: truthy parsing helpers and `NO_COLOR` presence checks.
- `shell`: POSIX single-quote escaping and ANSI stripping modes.
- `process`: command execution wrappers plus PATH lookup helpers.
- `git`: `git` command wrappers for repo probes and `rev-parse` helpers.
- `clipboard`: best-effort clipboard copy with explicit tool priority.
- `fs`: atomic write, timestamp write/remove, SHA-256 hashing, and cross-platform replace helpers
with structured errors.
- `greeting`: tiny sample helper used by `cli-template`.
## API examples
`env`:
```rust
use nils_common::env;
let starship_enabled = env::env_truthy_or("AGENTS_CLI_STARSHIP", false);
let no_color = env::no_color_enabled();
println!("starship={starship_enabled}, no_color={no_color}");
```
`shell`:
```rust
use nils_common::shell::{self, AnsiStripMode, SingleQuoteEscapeStyle};
let quoted = shell::quote_posix_single_with_style("a'b", SingleQuoteEscapeStyle::Backslash);
let plain = shell::strip_ansi("\x1b[31mred\x1b[0m", AnsiStripMode::CsiSgrOnly);
assert_eq!(quoted, "'a'\\''b'");
assert_eq!(plain, "red");
```
`process`:
```rust
use nils_common::process;
assert!(process::cmd_exists("git"));
let git_path = process::find_in_path("git").expect("git should be on PATH");
let out = process::run_stdout_trimmed(git_path.to_string_lossy().as_ref(), &["--version"])
.expect("git --version should run");
println!("{out}");
```
`git`:
```rust
use nils_common::git;
let inside = git::is_inside_work_tree().expect("git check should run");
if inside {
let root = git::repo_root().expect("repo root check");
println!("repo root: {root:?}");
}
```
`clipboard`:
```rust
use nils_common::clipboard::{copy_best_effort, ClipboardOutcome, ClipboardPolicy, ClipboardTool};
let tool_order = [
ClipboardTool::Pbcopy,
ClipboardTool::WlCopy,
ClipboardTool::Xclip,
ClipboardTool::Xsel,
ClipboardTool::Clip,
];
let outcome = copy_best_effort("hello", &ClipboardPolicy::new(&tool_order));
if matches!(
outcome,
ClipboardOutcome::SkippedNoTool | ClipboardOutcome::SkippedFailure
) {
eprintln!("clipboard copy unavailable; keep crate-local fallback messaging");
}
```
`fs`:
```rust
use nils_common::fs::{self, AtomicWriteError, SECRET_FILE_MODE};
use std::path::Path;
fs::write_atomic(Path::new("cache/auth.json"), br#"{"ok":true}"#, SECRET_FILE_MODE)?;
fs::write_timestamp(
Path::new("cache/auth.json.timestamp"),
Some("2026-02-01T00:00:00Z\n"),
)?;
let digest = fs::sha256_file(Path::new("cache/auth.json"))?;
if let Err(AtomicWriteError::CreateParentDir { path, .. }) =
fs::write_atomic(Path::new("/tmp/demo.json"), b"{}", SECRET_FILE_MODE)
{
eprintln!("parent directory error: {path:?}");
}
println!("sha256={digest}");
# Ok::<(), Box<dyn std::error::Error>>(())
```
## Migration conventions for parity
When introducing a shared helper at a call site:
1. Add or keep characterization tests in the caller crate first.
2. Move only primitive logic; keep a crate-local adapter for message formatting and exit-code mapping.
For `write_atomic` / `write_timestamp` / `sha256_file` migrations, map structured errors back to
existing crate-local UX text.
3. Preserve existing quote/ANSI mode choices and `NO_COLOR` behavior.
4. Keep tool/command fallback order identical (for example clipboard tool order, git probe fallback behavior).
5. Re-run crate tests that cover the touched command paths before merging.
## Non-goals
- Defining CLI-specific UX copy, warning templates, or emoji policy.
- Owning command-level business logic for a single CLI.
- Hiding meaningful behavior differences that should remain explicit in local adapters.
- Replacing specialized shared crates such as `api-testing-core`, `nils-term`, or `nils-test-support`.
## Docs
- [Docs index](docs/README.md)