Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@ jobs:
uv run python -c "from darnit.core.discovery import discover_implementations; print(discover_implementations())"
uv run python -c "from darnit_baseline import register; print(register())"

- name: Lint and smoke-test the quick-start script
run: |
shellcheck scripts/quickstart.sh
scripts/quickstart.sh --help

type-check:
name: Type Check
runs-on: ubuntu-latest
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- `scripts/quickstart.sh`: one-command setup from source. It installs uv and
zizmor if they are missing (asking first), installs darnit with the
versions pinned in `uv.lock`, runs an OpenSSF Baseline level 1 audit, and
offers to register the MCP server with Claude Code.
- `darnit install --from-source` registers the darnit executable of the
running installation as the MCP server command, with
`--framework openssf-baseline`. Without it,
`darnit install` writes `uvx --from darnit-mcp darnit serve`, which runs
the PyPI package even when darnit was installed from a source checkout.
- Operator configuration: a user-level TOML file found the same way by the
CLI, MCP server, and harness (`--operator-config PATH`, else
`$XDG_CONFIG_HOME/darnit/config.toml` or `~/.config/darnit/config.toml`,
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ Once [#229](https://github.com/kusari-oss/darnit/issues/229) lands, `pipx instal
and `uv tool install darnit-mcp` will become the recommended end-user paths, with
container, Homebrew, standalone-binary, and Claude Code plugin channels documented in
[`docs/install/README.md`](docs/install/README.md).

To set up from source with one command (it offers to install uv, sets darnit up, and audits a repository), see [One-command setup](docs/install/from-source.md#one-command-setup).

### Optional: Opengrep for taint analysis

The threat model generator can use [Opengrep](https://github.com/opengrep/opengrep)
Expand Down
3 changes: 3 additions & 0 deletions docs/getting-started/using-skills.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ darnit install --mcp-only

# Claude Desktop (GUI app) — different config file than Claude Code
darnit install --client claude-desktop

# From a source checkout: register this installation, not the PyPI package
darnit install --from-source
```

After installation, restart Claude Code. The skills appear as slash commands.
Expand Down
28 changes: 25 additions & 3 deletions docs/install/from-source.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,26 @@ For users who want to **try an unreleased feature**, contribute to darnit, or ru

This path is necessary today because darnit's packages aren't yet on PyPI. After the first stable release (`v0.1.0`), the published channels become the easier path; from-source becomes a contributor-or-feature-preview workflow.

## One-command setup

`scripts/quickstart.sh` does the steps on this page for you. It checks for git, offers to install uv and zizmor if they are missing, installs darnit with the dependency versions pinned in `uv.lock`, runs an OpenSSF Baseline level 1 audit on a repository, and offers to register the MCP server with Claude Code. It changes nothing in the audited repository.

From a checkout:

```bash
./scripts/quickstart.sh /path/to/your/repo
```

Without a checkout (the script clones darnit into `~/.local/share/darnit`):

```bash
curl -fsSL https://raw.githubusercontent.com/darnitdevorg/darnit/main/scripts/quickstart.sh -o quickstart.sh
less quickstart.sh # read it before you run it
bash quickstart.sh /path/to/your/repo
```

Options: `--yes` answers yes to every prompt, `--all-levels` audits all Baseline levels, `--no-mcp` skips the Claude Code registration, and `--dir DIR` sets where darnit is cloned. `--help` lists them.

## Quick install

```bash
Expand Down Expand Up @@ -54,14 +74,16 @@ darnit serve --framework openssf-baseline
Or configure it as an MCP server in Claude Code:

```bash
# Recommended: use darnit's install command (writes ~/.claude.json)
darnit install --client claude-code
# Recommended: use darnit's install command (writes ~/.claude.json).
# --from-source registers this checkout's darnit. Without it the entry is
# `uvx --from darnit-mcp darnit serve`, which runs the PyPI package instead.
darnit install --client claude-code --from-source

# Or register manually with the Claude Code CLI
claude mcp add --scope user darnit -- "$(which darnit)" serve --framework openssf-baseline
```

For a project-local MCP config, run `darnit install --project` from the repo root (writes `.mcp.json`).
For a project-local MCP config, run `darnit install --project --from-source` from the repo root (writes `.mcp.json`).

## Switching branches

Expand Down
43 changes: 39 additions & 4 deletions packages/darnit/src/darnit/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -638,6 +638,23 @@ def _install_skills(target_dir: Path, force: bool = False) -> int:
return installed


def _local_darnit_executable() -> Path | None:
"""Return the darnit console script of the running installation, if any.

Looks next to the running interpreter first, so the answer is the same
whether darnit was started from a virtual environment, through a symlink,
or by ``uv run``. Falls back to ``darnit`` on PATH.
"""
import shutil

name = "darnit.exe" if sys.platform == "win32" else "darnit"
candidate = Path(sys.executable).parent / name
if candidate.is_file():
return candidate
found = shutil.which("darnit")
return Path(found) if found else None


def cmd_install(args: argparse.Namespace) -> int:
"""Install darnit MCP server config and skills into a supported client."""
import shutil
Expand Down Expand Up @@ -690,10 +707,22 @@ def cmd_install(args: argparse.Namespace) -> int:
shutil.copy2(settings_path, backup_path)

mcp_servers = config.setdefault("mcpServers", {})
darnit_entry = {
"command": "uvx",
"args": ["--from", "darnit-mcp", "darnit", "serve"],
}
if getattr(args, "from_source", False):
darnit_exe = _local_darnit_executable()
if darnit_exe is None:
logger.error("--from-source: the darnit executable of this installation was not found.")
return 1
# Name the framework: a source checkout installs every workspace plugin,
# and `darnit serve` without --framework takes the first one it finds.
darnit_entry = {
"command": str(darnit_exe),
"args": ["serve", "--framework", "openssf-baseline"],
}
else:
darnit_entry = {
"command": "uvx",
"args": ["--from", "darnit-mcp", "darnit", "serve"],
}

if "darnit" in mcp_servers and not args.force:
response = input(f"'darnit' entry already exists in {settings_path}. Overwrite? [y/N]: ").strip().lower()
Expand Down Expand Up @@ -1745,6 +1774,12 @@ def create_parser() -> argparse.ArgumentParser:
help="Install skills into .claude/skills/ and MCP config into .mcp.json (per-project) instead of "
"user-scope paths. Not recommended: the repository then controls how darnit is launched.",
)
install_parser.add_argument(
"--from-source",
action="store_true",
help="Register the darnit executable of this installation (a source checkout or any other "
"non-PyPI install) instead of `uvx --from darnit-mcp`.",
)
install_parser.set_defaults(func=cmd_install)

return parser
Expand Down
233 changes: 233 additions & 0 deletions scripts/quickstart.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
#!/usr/bin/env bash
#
# quickstart.sh - Install darnit from source and run a first audit
#
# Usage:
# ./scripts/quickstart.sh [options] [path/to/repo]
#
# Requirements: git, and curl or wget if uv has to be installed.
# Nothing in the audited repository is modified.

set -euo pipefail

REPO_URL="https://github.com/darnitdevorg/darnit.git"
# The uv release installed when uv is missing. The script was tested with it.
UV_VERSION="0.13.0"
UV_INSTALLER="https://astral.sh/uv/${UV_VERSION}/install.sh"
ORIGINAL_PATH="$PATH"
DARNIT_HOME="${DARNIT_HOME:-$HOME/.local/share/darnit}"
ASSUME_YES=0
ALL_LEVELS=0
WANT_MCP=1
TARGET=""

if [ -t 1 ]; then
RED=$'\033[0;31m'; GREEN=$'\033[0;32m'; YELLOW=$'\033[1;33m'; NC=$'\033[0m'
else
RED=""; GREEN=""; YELLOW=""; NC=""
fi

info() { printf '%s==>%s %s\n' "$GREEN" "$NC" "$*"; }
warn() { printf '%swarning:%s %s\n' "$YELLOW" "$NC" "$*" >&2; }
die() { printf '%serror:%s %s\n' "$RED" "$NC" "$*" >&2; exit 1; }

usage() {
cat <<'USAGE'
Install darnit from source and run a first OpenSSF Baseline audit.

Usage: quickstart.sh [options] [path/to/repo]

1. Checks for git; offers to install uv and zizmor if they are missing
2. Uses the darnit checkout this script lives in, or clones one
3. Installs darnit into the checkout's virtual environment, using the
pinned versions in uv.lock, and links it into ~/.local/bin
4. Audits the given repository (default: the current directory, if it
is a git repository) against OpenSSF Baseline level 1
5. Offers to register the darnit MCP server with Claude Code

Options:
-y, --yes Answer yes to every prompt
--all-levels Audit all Baseline levels, not just level 1
--no-mcp Do not offer to register the MCP server
--dir DIR Where to clone darnit (default: ~/.local/share/darnit,
or $DARNIT_HOME)
-h, --help Show this help
USAGE
}

confirm() {
# confirm QUESTION [DEFAULT]; DEFAULT is y or n and is what Enter means.
local reply="" default="${2:-y}" hint="[Y/n]"
if [ "$default" = "n" ]; then
hint="[y/N]"
fi
if [ "$ASSUME_YES" -eq 1 ]; then
return 0
fi
if [ -t 0 ]; then
read -r -p "$1 $hint " reply || true
elif { : </dev/tty; } 2>/dev/null; then
read -r -p "$1 $hint " reply </dev/tty || true
else
warn "no terminal to ask on; assuming no. Re-run with --yes to accept."
return 1
fi
if [ -z "$reply" ]; then
reply="$default"
fi
case "$reply" in
y | Y | yes | YES) return 0 ;;
*) return 1 ;;
esac
}

main() {
while [ $# -gt 0 ]; do
case "$1" in
-y | --yes) ASSUME_YES=1 ;;
--all-levels) ALL_LEVELS=1 ;;
--no-mcp) WANT_MCP=0 ;;
--dir)
if [ $# -lt 2 ]; then die "--dir needs a path"; fi
DARNIT_HOME="$2"
shift
;;
-h | --help) usage; exit 0 ;;
-*) die "unknown option: $1 (try --help)" ;;
*)
if [ -n "$TARGET" ]; then die "only one repository path is accepted"; fi
TARGET="$1"
;;
esac
shift
done

# 1. Prerequisites
if ! command -v git >/dev/null 2>&1; then
die "git is required. Install it with your package manager and re-run."
fi
if ! command -v uv >/dev/null 2>&1; then
export PATH="$HOME/.local/bin:$PATH"
fi
if ! command -v uv >/dev/null 2>&1; then
info "uv is not installed. darnit uses it to manage Python and dependencies."
if ! confirm "Install uv $UV_VERSION from astral.sh into ~/.local/bin? Your shell profile is not changed." n; then
die "uv is required. See https://docs.astral.sh/uv/getting-started/installation/"
fi
if command -v curl >/dev/null 2>&1; then
curl -LsSf "$UV_INSTALLER" | env UV_NO_MODIFY_PATH=1 sh
elif command -v wget >/dev/null 2>&1; then
wget -qO- "$UV_INSTALLER" | env UV_NO_MODIFY_PATH=1 sh
else
die "curl or wget is needed to install uv."
fi
export PATH="$HOME/.local/bin:$PATH"
if ! command -v uv >/dev/null 2>&1; then
die "uv was installed but is not on PATH. Open a new shell and re-run."
fi
fi

# 2. Locate or clone the darnit source
local src="" self="${BASH_SOURCE[0]:-}" cand=""
if [ -n "$self" ] && [ -f "$self" ]; then
cand="$(cd "$(dirname "$self")/.." && pwd)"
if [ -d "$cand/packages/darnit" ]; then
src="$cand"
fi
fi
if [ -z "$src" ]; then
if [ -d "$DARNIT_HOME/packages/darnit" ]; then
info "Using the existing darnit checkout at $DARNIT_HOME"
else
info "Cloning darnit into $DARNIT_HOME"
git clone --depth 1 "$REPO_URL" "$DARNIT_HOME"
fi
src="$DARNIT_HOME"
fi

# 3. Install from the lockfile, so users get the tested dependency versions
info "Installing darnit from $src (the first run can take a minute)"
# --inexact: leave packages from other extras or groups in place, so a
# contributor's checkout synced with --all-extras is not stripped.
uv sync --frozen --inexact --quiet --project "$src"

local darnit="$src/.venv/bin/darnit"
if [ ! -x "$darnit" ]; then
die "the darnit executable was not found at $darnit"
fi
info "Installed $("$darnit" --version 2>&1 | tail -1)"

# Put darnit on PATH, without replacing anything that is not this link
local bindir="${XDG_BIN_HOME:-$HOME/.local/bin}" link
link="$bindir/darnit"
if [ ! -e "$link" ] && [ ! -L "$link" ]; then
mkdir -p "$bindir"
ln -s "$darnit" "$link"
fi
if [ "$(readlink "$link" 2>/dev/null || true)" != "$darnit" ]; then
warn "$link already exists and was left alone. Run $darnit directly, or remove it and re-run."
else
case ":$ORIGINAL_PATH:" in
*":$bindir:"*) ;;
*) warn "$bindir is not on your PATH. Add it (export PATH=\"$bindir:\$PATH\"), or run $darnit directly." ;;
esac
fi

# Optional tools that some checks shell out to
if ! command -v zizmor >/dev/null 2>&1; then
if confirm "Install zizmor (used by the GitHub Actions workflow checks) with uv?" n; then
uv tool install --quiet zizmor || warn "zizmor could not be installed."
fi
fi
if ! command -v zizmor >/dev/null 2>&1; then
warn "zizmor not found. The checks that use it will show as ERROR."
fi
if ! command -v gh >/dev/null 2>&1; then
warn "GitHub CLI (gh) not found. Checks that query GitHub will show as ERROR. Install it from https://cli.github.com/ and run 'gh auth login'."
elif ! gh auth token >/dev/null 2>&1; then
warn "GitHub CLI (gh) is not logged in. Checks that query GitHub will show as ERROR. Run 'gh auth login'."
fi

# 4. First audit
if [ -z "$TARGET" ] && [ -e "$PWD/.git" ] && [ "$(pwd -P)" != "$(cd "$src" && pwd -P)" ]; then
TARGET="$PWD"
fi
if [ -n "$TARGET" ]; then
if [ ! -d "$TARGET" ]; then die "not a directory: $TARGET"; fi
TARGET="$(cd "$TARGET" && pwd)"
local audit_args=(-q audit --no-fail --framework openssf-baseline)
if [ "$ALL_LEVELS" -eq 0 ]; then
audit_args+=(--tags level=1)
fi
if ! git -C "$TARGET" remote get-url origin >/dev/null 2>&1; then
warn "$TARGET has no 'origin' remote. Checks that query GitHub will show as ERROR until it is pushed."
fi
info "Auditing $TARGET"
"$darnit" "${audit_args[@]}" "$TARGET"
else
info "No repository given, so no audit was run."
fi

# 5. MCP registration (enables remediation through Claude Code)
if [ "$WANT_MCP" -eq 1 ]; then
if command -v claude >/dev/null 2>&1; then
if confirm "Register darnit in Claude Code? This replaces any darnit entry in ~/.claude.json and reinstalls the darnit skills in ~/.claude/skills/." n; then
if ! "$darnit" install --client claude-code --from-source --force; then
warn "registration failed. Retry with: darnit install --client claude-code --from-source"
fi
fi
else
info "Claude Code was not found, so the MCP server was not registered."
info "After installing it, run: darnit install --client claude-code --from-source"
fi
fi

printf '\n'
info "Done. Next steps:"
printf ' Audit a repo: darnit audit --framework openssf-baseline --tags level=1 /path/to/repo\n'
printf ' Fix findings: cd /path/to/repo && claude, then ask:\n'
printf ' "Audit this repo against the OpenSSF Baseline level 1, then show me\n'
printf ' the remediation plan as a dry run. Do not apply anything."\n'
}

main "$@"
Loading
Loading