#nils-cli #helper #workspace #shared #git

nils-common

Library crate for nils-common in the nils-cli workspace

23 unstable releases (3 breaking)

Uses new Rust 2024

new 0.6.3 Mar 2, 2026
0.6.2 Mar 2, 2026
0.5.9 Feb 28, 2026
0.4.9 Feb 23, 2026
0.3.9 Feb 14, 2026

#277 in Text processing

Download history 96/week @ 2026-02-09 33/week @ 2026-02-16 14/week @ 2026-02-23

143 downloads per month
Used in 24 crates (19 directly)

MIT license

125KB
3K SLoC

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.
  • GitHub adapters and gh command orchestration (for example issue/PR write workflows).

Modules and purpose

  • env: truthy parsing helpers, NO_COLOR checks, and trimmed non-empty env lookup.
  • shell: POSIX single-quote escaping and ANSI stripping modes.
  • process: command execution wrappers plus PATH lookup helpers.
  • git: git command wrappers for repo probes, rev-parse helpers, 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 by cli-template.

API examples

env:

use nils_common::env;

let starship_enabled = env::env_truthy_or("AGENTS_CLI_STARSHIP", false);
let no_color = env::no_color_enabled();
let maybe_agent_home = env::env_non_empty("AGENT_HOME");
println!("starship={starship_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>>(())

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.
  • 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, or nils-test-support.

Docs

Dependencies

~0.6–1.5MB
~30K SLoC