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.
Workspace-wide shared-crate boundary and extraction-lane decisions are documented in
docs/specs/workspace-shared-crate-boundary-v1.md.
Workspace-level keep/delete ownership decisions are tracked in
docs/specs/workspace-doc-retention-matrix-v1.md.
- 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.
- 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.
- GitHub adapters and
ghcommand orchestration (for example issue/PR write workflows).
env: truthy parsing helpers,NO_COLORchecks, and trimmed non-empty env lookup.shell: POSIX single-quote escaping and ANSI stripping modes.process: command execution wrappers plus PATH lookup helpers.git:gitcommand wrappers for repo probes,rev-parsehelpers, staged-path listing, and scope suggestion primitives for commit tooling.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.markdown: markdown payload validation, markdown-table-safe cell canonicalization, markdown heading/code-block rendering, and stable JSON pretty-format helpers used by orchestration/reporting CLIs.greeting: tiny sample helper used bycli-template.
env:
use nils_common::env;
let prompt_segment_enabled = env::env_truthy_or("AGENTS_CLI_PROMPT_SEGMENT", false);
let no_color = env::no_color_enabled();
let maybe_agent_home = env::env_non_empty("AGENT_HOME");
println!("prompt_segment={prompt_segment_enabled}, no_color={no_color}");shell:
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:
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:
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");
let staged = git::staged_name_only().expect("staged list");
let scope = git::suggested_scope_from_staged_paths(&staged);
println!("repo root: {root:?}");
println!("suggested scope: {scope}");
}clipboard:
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:
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>>(())When introducing a shared helper at a call site:
- Add or keep characterization tests in the caller crate first.
- Move only primitive logic; keep a crate-local adapter for message formatting and exit-code mapping. For
write_atomic/write_timestamp/sha256_filemigrations, map structured errors back to existing crate-local UX text. - Preserve existing quote/ANSI mode choices and
NO_COLORbehavior. - Keep tool/command fallback order identical (for example clipboard tool order, git probe fallback behavior).
- Re-run crate tests that cover the touched command paths before merging.
- Defining CLI-specific UX copy, warning templates, or emoji policy.
- Owning command-level business logic for a single CLI.
- Owning shared GitHub operation adapters such as
nils-common::github(keep crate-local adapters). - Hiding meaningful behavior differences that should remain explicit in local adapters.
- Replacing specialized shared crates such as
api-testing-core,nils-term, ornils-test-support.