From 070a4fa581ba6f95d77bbac26a942be1e2a97a42 Mon Sep 17 00:00:00 2001 From: Marc-cn <130138935+Marc-cn@users.noreply.github.com> Date: Fri, 9 Oct 2026 17:04:01 -0400 Subject: [PATCH 1/4] feat(cli): add darnit install --from-source darnit install always registers 'uvx --from darnit-mcp darnit serve' as the MCP server command. From a source checkout that runs the PyPI package, not the code that was installed. --from-source registers the darnit executable of the running installation instead: the console script next to the running interpreter, falling back to darnit on PATH. The default entry is unchanged. Signed-off-by: Marc-cn <130138935+Marc-cn@users.noreply.github.com> Assisted-by: Claude:claude-fable-5-1 --- CHANGELOG.md | 4 +++ docs/getting-started/using-skills.md | 3 +++ docs/install/from-source.md | 8 +++--- packages/darnit/src/darnit/cli.py | 38 +++++++++++++++++++++++--- tests/darnit/test_cli.py | 40 ++++++++++++++++++++++++++++ 5 files changed, 86 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e28cdf2..d3460087 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -143,6 +143,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- `darnit install --from-source` registers the darnit executable of the + running installation as the MCP server command. 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`, diff --git a/docs/getting-started/using-skills.md b/docs/getting-started/using-skills.md index 1e61a9fd..8d894df7 100644 --- a/docs/getting-started/using-skills.md +++ b/docs/getting-started/using-skills.md @@ -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. diff --git a/docs/install/from-source.md b/docs/install/from-source.md index fd7bd01f..697c324e 100644 --- a/docs/install/from-source.md +++ b/docs/install/from-source.md @@ -54,14 +54,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 diff --git a/packages/darnit/src/darnit/cli.py b/packages/darnit/src/darnit/cli.py index 20fd9668..45ae8235 100644 --- a/packages/darnit/src/darnit/cli.py +++ b/packages/darnit/src/darnit/cli.py @@ -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 @@ -690,10 +707,17 @@ 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 + darnit_entry = {"command": str(darnit_exe), "args": ["serve"]} + 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() @@ -1745,6 +1769,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 diff --git a/tests/darnit/test_cli.py b/tests/darnit/test_cli.py index 13472815..00f2d8dd 100644 --- a/tests/darnit/test_cli.py +++ b/tests/darnit/test_cli.py @@ -135,6 +135,46 @@ def test_install_preserves_existing_settings(tmp_path, monkeypatch): assert "darnit" in data["mcpServers"] +def test_install_from_source_registers_local_executable(tmp_path, monkeypatch): + monkeypatch.setattr("pathlib.Path.home", lambda: tmp_path) + bin_dir = tmp_path / "venv" / "bin" + bin_dir.mkdir(parents=True) + exe_name = "darnit.exe" if sys.platform == "win32" else "darnit" + (bin_dir / exe_name).write_text("") + monkeypatch.setattr(sys, "executable", str(bin_dir / "python")) + + exit_code = main(["install", "--mcp-only", "--from-source"]) + + assert exit_code == 0 + entry = json.loads((tmp_path / ".claude.json").read_text())["mcpServers"]["darnit"] + assert entry == {"command": str(bin_dir / exe_name), "args": ["serve"]} + + +def test_install_from_source_falls_back_to_path(tmp_path, monkeypatch): + monkeypatch.setattr("pathlib.Path.home", lambda: tmp_path) + on_path = tmp_path / "onpath" / "darnit" + monkeypatch.setattr(sys, "executable", str(tmp_path / "nowhere" / "python")) + monkeypatch.setattr("shutil.which", lambda name: str(on_path) if name == "darnit" else None) + + exit_code = main(["install", "--mcp-only", "--from-source"]) + + assert exit_code == 0 + entry = json.loads((tmp_path / ".claude.json").read_text())["mcpServers"]["darnit"] + assert entry == {"command": str(on_path), "args": ["serve"]} + + +def test_install_from_source_fails_when_executable_is_missing(tmp_path, monkeypatch, caplog): + monkeypatch.setattr("pathlib.Path.home", lambda: tmp_path) + monkeypatch.setattr(sys, "executable", str(tmp_path / "nowhere" / "python")) + monkeypatch.setattr("shutil.which", lambda name: None) + + exit_code = main(["install", "--mcp-only", "--from-source"]) + + assert exit_code == 1 + assert not (tmp_path / ".claude.json").exists() + assert any("--from-source" in record.message for record in caplog.records) + + class TestCreateParser: """Tests for CLI argument parsing.""" From 5c0485f4f7f563267752308f0b3164acf089f568 Mon Sep 17 00:00:00 2001 From: Marc-cn <130138935+Marc-cn@users.noreply.github.com> Date: Fri, 9 Oct 2026 17:45:01 -0400 Subject: [PATCH 2/4] fix(cli): name the framework in the --from-source entry A source checkout installs every workspace plugin, and darnit serve without --framework takes the first one it finds (community-spec). Claude Code then connected to a server that had no OpenSSF Baseline tools. Register 'serve --framework openssf-baseline', the same arguments as the manual command in docs/install/from-source.md. Found by running the registered server from Claude Code. Signed-off-by: Marc-cn <130138935+Marc-cn@users.noreply.github.com> Assisted-by: Claude:claude-fable-5-1 --- CHANGELOG.md | 3 ++- packages/darnit/src/darnit/cli.py | 7 ++++++- tests/darnit/test_cli.py | 4 ++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3460087..e6b38768 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -144,7 +144,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - `darnit install --from-source` registers the darnit executable of the - running installation as the MCP server command. Without it, + 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 diff --git a/packages/darnit/src/darnit/cli.py b/packages/darnit/src/darnit/cli.py index 45ae8235..197186e3 100644 --- a/packages/darnit/src/darnit/cli.py +++ b/packages/darnit/src/darnit/cli.py @@ -712,7 +712,12 @@ def cmd_install(args: argparse.Namespace) -> int: if darnit_exe is None: logger.error("--from-source: the darnit executable of this installation was not found.") return 1 - darnit_entry = {"command": str(darnit_exe), "args": ["serve"]} + # 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", diff --git a/tests/darnit/test_cli.py b/tests/darnit/test_cli.py index 00f2d8dd..c507ffb1 100644 --- a/tests/darnit/test_cli.py +++ b/tests/darnit/test_cli.py @@ -147,7 +147,7 @@ def test_install_from_source_registers_local_executable(tmp_path, monkeypatch): assert exit_code == 0 entry = json.loads((tmp_path / ".claude.json").read_text())["mcpServers"]["darnit"] - assert entry == {"command": str(bin_dir / exe_name), "args": ["serve"]} + assert entry == {"command": str(bin_dir / exe_name), "args": ["serve", "--framework", "openssf-baseline"]} def test_install_from_source_falls_back_to_path(tmp_path, monkeypatch): @@ -160,7 +160,7 @@ def test_install_from_source_falls_back_to_path(tmp_path, monkeypatch): assert exit_code == 0 entry = json.loads((tmp_path / ".claude.json").read_text())["mcpServers"]["darnit"] - assert entry == {"command": str(on_path), "args": ["serve"]} + assert entry == {"command": str(on_path), "args": ["serve", "--framework", "openssf-baseline"]} def test_install_from_source_fails_when_executable_is_missing(tmp_path, monkeypatch, caplog): From afe345a6c01a49ae4df42124a3f686a6007e377a Mon Sep 17 00:00:00 2001 From: Marc-cn <130138935+Marc-cn@users.noreply.github.com> Date: Fri, 9 Oct 2026 16:54:41 -0400 Subject: [PATCH 3/4] feat: add scripts/quickstart.sh for one-command setup Installs uv and zizmor if they are missing (asking first), uses the darnit checkout the script lives in or clones one, installs from uv.lock, runs an OpenSSF Baseline level 1 audit, and offers to register the MCP server with Claude Code through darnit install --from-source. Signed-off-by: Marc-cn <130138935+Marc-cn@users.noreply.github.com> Assisted-by: Claude:claude-fable-5-1 --- CHANGELOG.md | 4 + README.md | 2 + docs/install/from-source.md | 20 ++++ scripts/quickstart.sh | 222 ++++++++++++++++++++++++++++++++++++ 4 files changed, 248 insertions(+) create mode 100755 scripts/quickstart.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index e6b38768..aae0b8e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -143,6 +143,10 @@ 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, diff --git a/README.md b/README.md index c2a4da72..48a2b7d8 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,8 @@ The Baseline isn't just about security—it covers testing requirements, build p ## Installation +To try darnit from source with one command (it installs uv if needed, sets darnit up, and audits a repository), see [One-command setup](docs/install/from-source.md#one-command-setup). + > [!NOTE] > PyPI, pipx, `uv tool install`, container, Homebrew, and standalone-binary > channels are not published yet - tracked in diff --git a/docs/install/from-source.md b/docs/install/from-source.md index 697c324e..97e44525 100644 --- a/docs/install/from-source.md +++ b/docs/install/from-source.md @@ -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 diff --git a/scripts/quickstart.sh b/scripts/quickstart.sh new file mode 100755 index 00000000..6c208bb6 --- /dev/null +++ b/scripts/quickstart.sh @@ -0,0 +1,222 @@ +#!/usr/bin/env bash +# +# quickstart.sh - Install darnit from source and run a first audit +# +# Usage: +# ./scripts/quickstart.sh [options] [path/to/repo] +# curl -fsSL https://raw.githubusercontent.com/darnitdevorg/darnit/main/scripts/quickstart.sh \ +# | bash -s -- [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" +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() { + local reply="" + if [ "$ASSUME_YES" -eq 1 ]; then + return 0 + fi + if [ -t 0 ]; then + read -r -p "$1 [Y/n] " reply || true + elif { : /dev/null; then + read -r -p "$1 [Y/n] " reply /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 from https://astral.sh/uv now?"; 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 https://astral.sh/uv/install.sh | sh + elif command -v wget >/dev/null 2>&1; then + wget -qO- https://astral.sh/uv/install.sh | 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)" + uv sync --frozen --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 ":$PATH:" in + *":$bindir:"*) ;; + *) warn "$bindir is not on your PATH. Add it, 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?"; 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 (writes ~/.claude.json, replacing any existing darnit entry)?"; 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 "$@" From a4afaa7e6a4c5b823196b579e59c0718e5056bac Mon Sep 17 00:00:00 2001 From: Marc-cn <130138935+Marc-cn@users.noreply.github.com> Date: Sat, 10 Oct 2026 18:59:57 -0400 Subject: [PATCH 4/4] fix(quickstart): address review - Install a pinned uv with UV_NO_MODIFY_PATH=1, so the installer does not edit shell profiles; print the PATH line instead, and say so in the prompt. - Remove the curl-pipe-bash usage from the header. The docs say to read the script first. - The registration prompt now says that the darnit skills in ~/.claude/skills are reinstalled, not only that ~/.claude.json changes. - uv sync --inexact, so a checkout synced with --all-extras keeps its packages (116 before and after; it lost 33 without it). - Prompts default to no. - Move the README pointer below the published channels. - CI: shellcheck and --help on the script in the Lint job. Signed-off-by: Marc-cn <130138935+Marc-cn@users.noreply.github.com> Assisted-by: Claude:claude-fable-5-1 --- .github/workflows/ci.yml | 5 +++++ README.md | 5 +++-- scripts/quickstart.sh | 39 +++++++++++++++++++++++++-------------- 3 files changed, 33 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f7b72a83..d21c5882 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/README.md b/README.md index 48a2b7d8..5f2994f9 100644 --- a/README.md +++ b/README.md @@ -37,8 +37,6 @@ The Baseline isn't just about security—it covers testing requirements, build p ## Installation -To try darnit from source with one command (it installs uv if needed, sets darnit up, and audits a repository), see [One-command setup](docs/install/from-source.md#one-command-setup). - > [!NOTE] > PyPI, pipx, `uv tool install`, container, Homebrew, and standalone-binary > channels are not published yet - tracked in @@ -59,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) diff --git a/scripts/quickstart.sh b/scripts/quickstart.sh index 6c208bb6..ef94c64b 100755 --- a/scripts/quickstart.sh +++ b/scripts/quickstart.sh @@ -4,8 +4,6 @@ # # Usage: # ./scripts/quickstart.sh [options] [path/to/repo] -# curl -fsSL https://raw.githubusercontent.com/darnitdevorg/darnit/main/scripts/quickstart.sh \ -# | bash -s -- [options] [path/to/repo] # # Requirements: git, and curl or wget if uv has to be installed. # Nothing in the audited repository is modified. @@ -13,6 +11,10 @@ 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 @@ -54,20 +56,27 @@ USAGE } confirm() { - local reply="" + # 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 [Y/n] " reply || true + read -r -p "$1 $hint " reply || true elif { : /dev/null; then - read -r -p "$1 [Y/n] " reply /dev/null 2>&1; then info "uv is not installed. darnit uses it to manage Python and dependencies." - if ! confirm "Install uv from https://astral.sh/uv now?"; then + 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 https://astral.sh/uv/install.sh | sh + curl -LsSf "$UV_INSTALLER" | env UV_NO_MODIFY_PATH=1 sh elif command -v wget >/dev/null 2>&1; then - wget -qO- https://astral.sh/uv/install.sh | sh + wget -qO- "$UV_INSTALLER" | env UV_NO_MODIFY_PATH=1 sh else die "curl or wget is needed to install uv." fi @@ -138,7 +147,9 @@ main() { # 3. Install from the lockfile, so users get the tested dependency versions info "Installing darnit from $src (the first run can take a minute)" - uv sync --frozen --quiet --project "$src" + # --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 @@ -156,15 +167,15 @@ main() { 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 ":$PATH:" in + case ":$ORIGINAL_PATH:" in *":$bindir:"*) ;; - *) warn "$bindir is not on your PATH. Add it, or run $darnit directly." ;; + *) 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?"; 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 @@ -200,7 +211,7 @@ main() { # 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 (writes ~/.claude.json, replacing any existing darnit entry)?"; 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