From 75c6ea1dd56663d1c8d06e344882c4250814266f Mon Sep 17 00:00:00 2001 From: Defame1297 Date: Sat, 20 Jun 2026 23:16:27 +0000 Subject: [PATCH] feat(skills): add neuledge-context skill for @neuledge/context MCP server MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a complete cross-cutting skill to install, configure, and manage @neuledge/context — a local-first MCP server that delivers version-specific library docs to AI agents via SQLite FTS5. Includes: - SKILL.md with 9-step process: install, global MCP registration, per-project --libs scoping, package management, auth, custom registry, upgrade, uninstall - setup-neuledge-context.sh: pinned version install, idempotent version check - secure-context-config.sh: chmod 600 on ~/.context/config.json after auth - 13-case test suite covering both scripts (all pass) - eval.yaml with 6 trigger tests and 4 output tests - references/: context-cli-reference.md, http-mode.md, install-notes.md Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01TP4EGbBg3XMcyF28Lx78XJ --- .../cross-cutting/neuledge-context/eval.yaml | 97 +++++++ .agents/skills/neuledge-context/META.md | 12 + .agents/skills/neuledge-context/SKILL.md | 108 ++++++++ .../references/context-cli-reference.md | 248 ++++++++++++++++++ .../neuledge-context/references/http-mode.md | 53 ++++ .../references/install-notes.md | 72 +++++ scripts/secure-context-config.sh | 16 ++ scripts/setup-neuledge-context.sh | 36 +++ tests/test-neuledge-context.sh | 208 +++++++++++++++ 9 files changed, 850 insertions(+) create mode 100644 .agents/evals/cross-cutting/neuledge-context/eval.yaml create mode 100644 .agents/skills/neuledge-context/META.md create mode 100644 .agents/skills/neuledge-context/SKILL.md create mode 100644 .agents/skills/neuledge-context/references/context-cli-reference.md create mode 100644 .agents/skills/neuledge-context/references/http-mode.md create mode 100644 .agents/skills/neuledge-context/references/install-notes.md create mode 100755 scripts/secure-context-config.sh create mode 100755 scripts/setup-neuledge-context.sh create mode 100755 tests/test-neuledge-context.sh diff --git a/.agents/evals/cross-cutting/neuledge-context/eval.yaml b/.agents/evals/cross-cutting/neuledge-context/eval.yaml new file mode 100644 index 0000000..3f2f62c --- /dev/null +++ b/.agents/evals/cross-cutting/neuledge-context/eval.yaml @@ -0,0 +1,97 @@ +skill_name: neuledge-context + +trigger_tests: + - id: explicit-install-register + name: "Explicit trigger — install and register" + query: "install @neuledge/context and register it as an MCP server in Claude Code" + should_trigger: true + + - id: explicit-package-management + name: "Explicit trigger — package management" + query: "install the react documentation package using neuledge context" + should_trigger: true + + - id: implicit-offline-docs + name: "Implicit trigger — offline docs for AI agent" + query: "I need React and Next.js docs available to my AI agent without web searches" + should_trigger: true + + - id: negative-different-mcp + name: "Negative — different MCP server" + query: "Add the Gitea MCP server to Claude Code" + should_trigger: false + + - id: negative-query-existing + name: "Negative — querying an already-running server" + query: "How do I query React docs using the context server that's already running?" + should_trigger: false + + - id: negative-cursor-setup + name: "Negative — different provider" + query: "Set up context serve for Cursor" + should_trigger: false + +output_tests: + - id: install-script-announced + name: "Install — script announced before running, version verified after" + type: deterministic + prompt: "install @neuledge/context" + expected_output: > + The skill announces that it will run scripts/setup-neuledge-context.sh before executing it, + then verifies the installation by running `context --version`. + assertions: + - "Output mentions 'scripts/setup-neuledge-context.sh' before any install command is run" + - "Output includes a `context --version` call after the install step" + - "Output does not contain `npm install -g @neuledge/context@latest` (no floating @latest)" + + - id: mcp-list-before-add + name: "MCP registration — list checked before add, skipped if present" + type: deterministic + prompt: "register @neuledge/context as a Claude Code MCP server" + expected_output: > + The skill runs `claude mcp list` and checks for an existing 'context' entry before + running `claude mcp add`. If already registered, the add step is skipped. + assertions: + - "Output includes `claude mcp list` before `claude mcp add context`" + - "Output states that registration is skipped when the server is already present" + - "The `claude mcp add` command uses stdio form: `claude mcp add context -- context serve`" + + - id: auth-chmod-paired + name: "Auth — secure-context-config.sh run immediately after auth add" + type: deterministic + prompt: "add auth credentials for docs.example.com to neuledge context" + expected_output: > + The skill runs `context auth add docs.example.com` with an environment variable reference + for the credential, then immediately runs scripts/secure-context-config.sh. + No credential value appears in the output. + assertions: + - "Output references an environment variable (e.g. $TOKEN) rather than a literal credential value" + - "Output runs `scripts/secure-context-config.sh` in the same step as or immediately after `context auth add`" + - "No bearer token, cookie value, or other credential string appears in the output" + + - id: git-check-url-source + name: "context add — git prerequisite checked for URL sources only" + type: deterministic + prompt: "add documentation from https://github.com/prisma/prisma using context add" + expected_output: > + Before running `context add`, the skill checks `git --version` because the source is a + GitHub URL. The check is present for URL/repo sources and absent for local .db file paths. + assertions: + - "Output includes `git --version` before the `context add https://github.com/...` command" + - "If given a local .db file path instead, the git check is absent" + + - id: install-flow-quality + name: "Full install + register flow quality" + type: llm-rubric + prompt: "install @neuledge/context and set it up as my Claude Code MCP server" + expected_output: > + A complete, ordered install-then-register flow: (1) announce the install script, + (2) run setup-neuledge-context.sh, (3) verify with context --version, + (4) check claude mcp list, (5) run claude mcp add if not already registered, + (6) confirm with claude mcp list. Steps are in the correct order with verification + between install and registration. + assertions: + - "Install step comes before MCP registration step" + - "A verification command (context --version) appears between install and registration" + - "The output would leave a user with a working @neuledge/context MCP server in Claude Code" + - "No step is skipped without an explanation of why it was skipped" diff --git a/.agents/skills/neuledge-context/META.md b/.agents/skills/neuledge-context/META.md new file mode 100644 index 0000000..fd14db1 --- /dev/null +++ b/.agents/skills/neuledge-context/META.md @@ -0,0 +1,12 @@ +```yaml +version: "1.0" +updated: 2026-06-20 + +when: Invoked when the user asks to install @neuledge/context, wire it as a Claude Code MCP server, manage documentation packages from the registry or custom sources, configure custom registry servers, or manage private registry auth. Also invoked when the user wants to scope an MCP session to specific libraries via --libs. + +references: + # README — source for references/context-cli-reference.md, references/http-mode.md, references/install-notes.md + - https://github.com/neuledge/context + # Download Server API spec — source for self-hosted registry section in references/install-notes.md + - https://github.com/neuledge/context/blob/main/SERVER_SPEC.md +``` diff --git a/.agents/skills/neuledge-context/SKILL.md b/.agents/skills/neuledge-context/SKILL.md new file mode 100644 index 0000000..d6b444d --- /dev/null +++ b/.agents/skills/neuledge-context/SKILL.md @@ -0,0 +1,108 @@ +--- +name: neuledge-context +description: "Use when the user wants to install @neuledge/context, register it as a Claude Code MCP server, manage documentation packages (install, browse, add, remove, list), configure custom registries, or manage auth for private registries. Do NOT use when the user wants to install a different MCP server, wants to query docs from an already-running context server (call `get_docs` directly), or is setting up @neuledge/context for Cursor, VS Code Copilot, or Claude Desktop rather than Claude Code." +metadata: + category: cross-cutting +allowed-tools: + - Bash + - Read + - Edit +--- + + + +## Required inputs + +- **Task type** — install / register-mcp / package-management / auth / upgrade / uninstall; inferred from user request, ask if ambiguous +- **Package name(s)** (package management only) — name(s) to install, add, or remove; ask if not stated +- **Library list** (project MCP scope only) — space-separated lib names for `--libs`; ask if not stated +- **Domain** (auth only) — domain to authenticate against; ask if not stated +- **Credential reference** (auth only) — environment variable or secret manager path holding the token or cookie; never the raw value + +## Constraints + +- Run `scripts/setup-neuledge-context.sh ` for install and upgrade — never run `npm install -g @neuledge/context@latest` +- Run `scripts/secure-context-config.sh` immediately after every `context auth add` — never skip the permissions step +- Check `git --version` before running `context add` on a URL or GitHub source; skip the check for local `.db` file paths +- Never echo, repeat, or log auth token or cookie values — reference the environment variable name only +- Scope all steps to stdio mode; HTTP/Docker mode is documented in `references/http-mode.md`, not covered here +- State what you are about to do before running `setup-neuledge-context.sh` — it modifies global npm and Claude Code MCP config + + + + + +## Process + +1. **Identify task.** Infer the task type from the user's request. If the request spans multiple tasks (e.g. install + package install), execute them in the order listed below. + +2. **Install.** (Skip if `context --version` already returns the target version.) + - State: "I will run `scripts/setup-neuledge-context.sh` from the ai-development repo root — this installs `@neuledge/context` globally at the pinned version." + - Run: `bash scripts/setup-neuledge-context.sh` from `/root/ai-development/` + - Verify: `context --version` + - See `references/install-notes.md` for prerequisites and native SQLite build tool requirements. + +3. **Register global MCP server.** (One-time per machine. Skip if already registered.) + - Check: `claude mcp list | grep '^context '` — if output is non-empty, skip. + - Run: `claude mcp add context -- context serve` + - Confirm: `claude mcp list` + +4. **Scope MCP to specific libraries (optional, per-project).** (When user wants to restrict the MCP session to pre-approved libraries for a project.) + - Collect the library list from the user if not stated. + - For each named library, run `context browse ` to confirm the exact package identifier (e.g. `react` vs `npm/react`, `next` vs `nextjs`). Use the identifier shown in the browse output — do not guess. If `context list` shows the package is already installed, that identifier is authoritative. + - Run: `claude mcp add --project context -- context serve --libs ...` + - Note: this overrides the global registration for the current project only. The `search_packages` and `download_package` MCP tools are hidden; only `get_docs` is available for the listed libraries. + +5. **Manage packages.** + - **Browse registry:** `context browse ` — shows available versions. + - **Install from registry:** `context install [version]` — e.g. `context install npm/react`. + - **Add from source:** Check `git --version` first if source is a URL or GitHub repo. Run: `context add [--path ] [--tag ] [--name ]`. See `references/context-cli-reference.md` for all flags. + - **List installed:** `context list` + - **Remove:** `context remove @` to target a specific version; `context remove ` removes all versions — confirm with the user before removing all. + +6. **Configure auth.** (Private registries or subscriber-only content.) + - Run: `context auth add --header "Authorization: Bearer $TOKEN"` — use the environment variable name, not the value. + - Immediately run: `bash scripts/secure-context-config.sh` from `/root/ai-development/` + +7. **Configure custom registry.** (Self-hosted registry server.) + - Read `~/.context/config.json`. Add a server entry following the schema in `references/context-cli-reference.md`. + - Test: `context browse --server ` + +8. **Upgrade.** Run `bash scripts/setup-neuledge-context.sh ` from `/root/ai-development/`. The script checks the current version and skips if already at target. + +9. **Uninstall.** + - Run: `npm uninstall -g @neuledge/context` + - Ask the user before removing downloaded packages: `rm -rf ~/.context` deletes all installed documentation and config — confirm before running. + +## Output format + +No structured output file. The skill produces terminal confirmation of each completed step. Artifacts: +- `@neuledge/context` installed globally at pinned version +- MCP server entry in `~/.claude/settings.json` (global) or `.claude/settings.local.json` (project) +- Downloaded `.db` packages in `~/.context/packages/` +- Modified `~/.context/config.json` (auth entries, registry config) + + + + + +## Failure handling + +- `scripts/setup-neuledge-context.sh` not found — stop; instruct user to run from `/root/ai-development/` +- `context` binary not found after install — report npm error; check `node --version` is ≥18 +- MCP registration fails — run `claude mcp list` to check for name conflicts; if `context` is already registered with different args, report it and ask the user to remove it first with `claude mcp remove context` +- `context add` fails with "git not found" — advise `apt-get install git` (Debian/Ubuntu) or equivalent; re-run after install +- Native SQLite build failure — Context falls back to WASM automatically; advise installing build tools for better performance: `apt-get install -y python3 make g++` on Debian/Ubuntu. See `references/install-notes.md` for known gotchas. + +## Self-check + +- [ ] Task type identified before any commands run +- [ ] `setup-neuledge-context.sh` announced before execution +- [ ] `claude mcp list` checked before `claude mcp add` — skipped if already registered +- [ ] `git --version` checked before `context add` on a URL or GitHub source +- [ ] Auth step: `secure-context-config.sh` run immediately after every `context auth add` +- [ ] No credential values echoed or logged — environment variable references only +- [ ] `context remove @` used (not bare name) when targeting a specific version +- [ ] `rm -rf ~/.context` confirmed with user before running + + diff --git a/.agents/skills/neuledge-context/references/context-cli-reference.md b/.agents/skills/neuledge-context/references/context-cli-reference.md new file mode 100644 index 0000000..155ec92 --- /dev/null +++ b/.agents/skills/neuledge-context/references/context-cli-reference.md @@ -0,0 +1,248 @@ +# Context CLI Reference + +Binary: `context` +Package: `@neuledge/context` + +--- + +## `context add ` + +Build and install a documentation package from any source. + +**Arguments:** +- `` — Package source. Auto-detected type: + - Local `.db` file path → copies directly + - HTTP(S) URL ending in `.db` → downloads + - GitHub URL or git repo URL → clones and builds + - Local directory path → builds from local docs + - Website URL → fetches `llms.txt` and builds + +**Options:** + +| Flag | Description | +|------|-------------| +| `--tag ` | Git tag to checkout (for git repos) | +| `--pkg-version ` | Custom version label for the built package | +| `--path ` | Path to docs folder within the repo or directory | +| `--name ` | Custom package name (overrides auto-detected name) | +| `--save ` | Save a copy of the built `.db` file to this location | +| `--lang ` | Language filter: `all` for all languages, or ISO code (`en`, `de`, etc.) | + +**Examples:** +```bash +context add https://github.com/prisma/prisma +context add ./my-local-docs --name mylib --pkg-version 2.0 +context add https://mysite.com/docs --lang en +context add /tmp/react-19.0.db +``` + +--- + +## `context install [version]` + +Download and install a pre-built package from the registry. + +**Arguments:** +- `` — Package identifier, e.g. `npm/next` or `next` +- `[version]` — Specific version (latest used if omitted) + +**Options:** + +| Flag | Description | +|------|-------------| +| `--server ` | Named server from config (default: `neuledge`) | + +**Examples:** +```bash +context install npm/next +context install npm/react 18.0.0 +context install prisma --server myserver +``` + +--- + +## `context browse ` + +Search for packages available on the registry. + +**Arguments:** +- `` — Package name or `registry/name`, e.g. `npm/next` or just `react` + +**Options:** + +| Flag | Description | +|------|-------------| +| `--server ` | Named server from config | + +**Examples:** +```bash +context browse react +context browse npm/next +``` + +--- + +## `context list` + +Display all installed packages with sizes and section counts. + +```bash +context list +``` + +--- + +## `context remove ` + +Remove an installed documentation package. + +**Arguments:** +- `` — Package name or name with version, e.g. `next` or `next@v16.2.0` + +**Examples:** +```bash +context remove next +context remove next@v16.2.0 +``` + +--- + +## `context serve` + +Start the MCP server. + +**Options:** + +| Flag | Description | +|------|-------------| +| `--http [port]` | Run as HTTP server (default port: 8080) instead of stdio | +| `--host ` | Binding address for HTTP mode (default: `127.0.0.1`) | +| `--libs ` | Restrict session to specific libraries; hides search/download tools | + +**Examples:** +```bash +context serve # stdio mode (for Claude Code, Cursor, etc.) +context serve --http # HTTP on port 8080 +context serve --http 3000 # HTTP on port 3000 +context serve --http --host 0.0.0.0 # HTTP accessible on all interfaces +context serve --libs react next # locked to react and next only +``` + +--- + +## `context query ` + +Query documentation from an installed package via CLI (no MCP needed). + +**Arguments:** +- `` — Installed package in `name@version` format, e.g. `nextjs@15.0` +- `` — Documentation topic, e.g. `createServer`, `cors middleware` + +**Examples:** +```bash +context query nextjs@15.0 "app router" +context query react@18.0 useState +``` + +--- + +## `context auth` + +Manage per-domain authentication for subscriber-only or private content. + +### `context auth add ` + +Add or update authentication for a domain. + +**Arguments:** +- `` — Domain to authenticate against + +**Options:** + +| Flag | Description | +|------|-------------| +| `--cookies ` | Cookie header value | +| `--header
` | Custom HTTP header (e.g. `Authorization: Bearer token`) | + +**Example:** +```bash +context auth add docs.example.com --header "Authorization: Bearer mytoken" +context auth add private.site.com --cookies "session=abc123" +``` + +### `context auth list` + +Display all configured authentication entries. + +### `context auth remove ` + +Delete authentication for a domain. + +--- + +## Config File + +Location: `~/.context/config.json` +Format: JSON + +```json +{ + "servers": [ + { + "name": "neuledge", + "url": "https://api.context.neuledge.com", + "default": true + } + ] +} +``` + +**Config keys:** + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `servers` | array | single Neuledge entry | Registry server configurations | +| `servers[].name` | string | `"neuledge"` | Identifier used with `--server` flag | +| `servers[].url` | string | `"https://api.context.neuledge.com"` | Server endpoint | +| `servers[].default` | boolean | `true` | Primary server used when `--server` is omitted | + +To add a self-hosted registry server: +```json +{ + "servers": [ + { "name": "neuledge", "url": "https://api.context.neuledge.com", "default": true }, + { "name": "myorg", "url": "https://context.myorg.internal" } + ] +} +``` + +--- + +## Environment Variables + +None documented. Config is file-based (`~/.context/config.json`). + +--- + +## MCP Tool Reference (server-side) + +When running `context serve`, these MCP tools are registered: + +### `get_docs` +Primary documentation lookup. Use before web searches when the library is installed. +- `library` — `name@version`, e.g. `react@18.0` +- `topic` — Short API name or keyword, e.g. `useState`, `cors middleware` + +### `search_packages` +Find available packages in a registry. +- `registry` — `npm`, `pip`, `cargo`, `go` +- `name` — Package name +- `version` (optional) — Specific version +- `server` (optional) — Named server from config + +### `download_package` +Download and install a package from the registry. +- `registry` — Registry identifier +- `name` — Package name +- `version` — Version string +- `server` (optional) — Named server from config diff --git a/.agents/skills/neuledge-context/references/http-mode.md b/.agents/skills/neuledge-context/references/http-mode.md new file mode 100644 index 0000000..9428a18 --- /dev/null +++ b/.agents/skills/neuledge-context/references/http-mode.md @@ -0,0 +1,53 @@ +# HTTP / Docker Mode + +`context serve` can run as an HTTP MCP server instead of stdio. Use this when you need to share a single context server across multiple agents, run it in a container, or expose it to remote clients. + +## Start as HTTP server + +```bash +# Localhost only (default) +context serve --http + +# Specific port +context serve --http 3000 + +# All interfaces (for Docker or remote access) +context serve --http 8080 --host 0.0.0.0 +``` + +## Docker + +```bash +docker run -p 8080:8080 \ + -v ~/.context:/root/.context \ + neuledge/context \ + context serve --http 8080 --host 0.0.0.0 +``` + +The volume mount (`-v ~/.context:/root/.context`) reuses packages already downloaded on the host. Omit it if you want an isolated container with its own package store. + +## Client configuration + +Point remote agents at `http://:8080` as an HTTP MCP server. For Claude Desktop, Cursor, or VS Code Copilot, use the same JSON structure as stdio mode but set `url` instead of `command`: + +```json +{ + "mcpServers": { + "context": { + "url": "http://localhost:8080" + } + } +} +``` + +## Security note + +HTTP mode has no built-in authentication. Do not expose `--host 0.0.0.0` on a public interface without a reverse proxy or firewall rule. On shared systems, bind to `127.0.0.1` (the default) and use an SSH tunnel if remote access is needed. + +## Scope restriction + +`--libs` works in HTTP mode the same as in stdio mode: + +```bash +context serve --http 8080 --host 0.0.0.0 --libs react next typescript +``` diff --git a/.agents/skills/neuledge-context/references/install-notes.md b/.agents/skills/neuledge-context/references/install-notes.md new file mode 100644 index 0000000..ae07306 --- /dev/null +++ b/.agents/skills/neuledge-context/references/install-notes.md @@ -0,0 +1,72 @@ +# Install Notes + +## Prerequisites + +- Node.js LTS (≥18) +- npm — pnpm/yarn also work + +No other system dependencies are required. The package uses a WASM SQLite fallback (`sql.js-fts5`) if the native `better-sqlite3` module fails to build — but native mode is significantly faster. + +**For native SQLite (recommended on Linux servers):** +```bash +apt-get install -y python3 make g++ # Debian/Ubuntu +``` + +--- + +## File Locations + +| Path | Purpose | +|------|---------| +| `~/.context/packages/` | Downloaded/built SQLite package files (`name@version.db`) | +| `~/.context/config.json` | Server configuration (registries, auth credentials) | + +Both directories are created automatically on first use. + +--- + +## Self-Hosted Registry Server + +Anyone can operate a compatible registry implementing the [Download Server API](https://github.com/neuledge/context/blob/main/SERVER_SPEC.md): + +- `GET /search?registry=&name=[&version=]` — search packages +- `GET /packages///` — package metadata +- `GET /packages////download` — download `.db` file +- `POST /packages///` — publish (requires Bearer token) + +Register a custom server in `~/.context/config.json` and reference it with `--server `. + +--- + +## Upgrade / Uninstall + +```bash +# Upgrade — pass the target version to the install script +bash scripts/setup-neuledge-context.sh + +# Uninstall binary +npm uninstall -g @neuledge/context + +# Remove all downloaded packages and config (optional — confirm with user first) +rm -rf ~/.context +``` + +No migration needed on upgrade — `.db` package files are not affected by CLI version changes. + +--- + +## Known Gotchas + +1. **Native build failure** — If `better-sqlite3` fails to compile (missing Python/g++), Context falls back to WASM automatically. Queries still work but are slower. Install build tools to fix. + +2. **Package naming** — `context remove next` removes all versions of `next`. Use `next@v16.2.0` to target a specific version. + +3. **`context add` for git repos** — Requires `git` to be installed. Clones the full repo; use `--path` to limit to a docs subdirectory and `--tag` to pin a version. + +4. **Registry package IDs** — Registry identifiers follow `/` format (e.g. `npm/react`, `pip/fastapi`, `cargo/tokio`). The `browse` command accepts bare names (`react`) and infers the registry. + +5. **`--libs` restriction** — When `context serve --libs react next`, the `search_packages` and `download_package` MCP tools are hidden from the agent. Only `get_docs` is available, scoped to the listed libraries. + +6. **No releases on GitHub** — The project publishes via npm directly. Check `npm view @neuledge/context version` for the current version. + +7. **Auth cookies/headers** — Stored in `~/.context/config.json` as plain JSON. File permissions matter — always run `scripts/secure-context-config.sh` after `context auth add`. diff --git a/scripts/secure-context-config.sh b/scripts/secure-context-config.sh new file mode 100755 index 0000000..8803482 --- /dev/null +++ b/scripts/secure-context-config.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Sets secure file permissions on ~/.context/config.json. +# Run after every `context auth add` to protect stored credentials. +# Safe to run when the file does not exist yet. + +CONFIG="${HOME}/.context/config.json" + +if [ ! -f "$CONFIG" ]; then + echo "Skipped: ${CONFIG} does not exist — no permissions to set." + exit 0 +fi + +chmod 600 "$CONFIG" +echo "Secured: ${CONFIG} set to 600 (owner read/write only)." diff --git a/scripts/setup-neuledge-context.sh b/scripts/setup-neuledge-context.sh new file mode 100755 index 0000000..8ddc6e4 --- /dev/null +++ b/scripts/setup-neuledge-context.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Installs @neuledge/context globally at a pinned version. +# Usage: setup-neuledge-context.sh [VERSION] +# VERSION — npm version string (default: 1.2.0) +# Idempotent: skips install if already at the target version. + +TARGET_VERSION="${1:-1.2.0}" + +current_version() { + context --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "" +} + +CURRENT="$(current_version)" + +if [ "$CURRENT" = "$TARGET_VERSION" ]; then + echo "Already at @neuledge/context@${TARGET_VERSION} — nothing to do." + exit 0 +fi + +if [ -n "$CURRENT" ]; then + echo "Upgrading @neuledge/context from ${CURRENT} to ${TARGET_VERSION}..." +else + echo "Installing @neuledge/context@${TARGET_VERSION}..." +fi + +npm install -g "@neuledge/context@${TARGET_VERSION}" + +INSTALLED="$(current_version)" +if [ "$INSTALLED" = "$TARGET_VERSION" ]; then + echo "Done: @neuledge/context@${TARGET_VERSION} installed." +else + echo "Error: expected version ${TARGET_VERSION} but got '${INSTALLED}'" >&2 + exit 1 +fi diff --git a/tests/test-neuledge-context.sh b/tests/test-neuledge-context.sh new file mode 100755 index 0000000..c6c10ed --- /dev/null +++ b/tests/test-neuledge-context.sh @@ -0,0 +1,208 @@ +#!/usr/bin/env bash +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +INSTALL_SCRIPT="$REPO_ROOT/scripts/setup-neuledge-context.sh" +SECURE_SCRIPT="$REPO_ROOT/scripts/secure-context-config.sh" +PASS=0 +FAIL=0 + +pass() { echo " PASS: $1"; PASS=$((PASS + 1)); } +fail() { echo " FAIL: $1"; FAIL=$((FAIL + 1)); } + +# Fake bin directory — npm and context stubs go here +FAKE_BIN="$(mktemp -d)" +trap 'rm -rf "$FAKE_BIN"' EXIT + +# --- setup-neuledge-context.sh --- + +echo "" +echo "--- install: already at target version, skips npm ---" + +# context returns target version; npm must NOT be called +printf '#!/bin/sh\necho "1.2.0"\n' > "$FAKE_BIN/context"; chmod +x "$FAKE_BIN/context" +printf '#!/bin/sh\necho "npm called unexpectedly" >&2; exit 1\n' > "$FAKE_BIN/npm"; chmod +x "$FAKE_BIN/npm" + +OUT="$(PATH="$FAKE_BIN:$PATH" bash "$INSTALL_SCRIPT" "1.2.0" 2>&1)" +if echo "$OUT" | grep -q "nothing to do"; then + pass "skips install when already at target version" +else + fail "expected 'nothing to do' — got: $OUT" +fi + +echo "" +echo "--- install: missing version, runs npm ---" + +# context not found; npm installs and context then returns the version +rm -f "$FAKE_BIN/context" +printf '#!/bin/sh\necho ""\n' > "$FAKE_BIN/context"; chmod +x "$FAKE_BIN/context" +# npm installs and makes context return the right version on next call +FAKE_BIN2="$(mktemp -d)" +trap 'rm -rf "$FAKE_BIN2"' EXIT +cat > "$FAKE_BIN2/npm" <<'SH' +#!/bin/sh +# After npm runs, replace the context stub to return the installed version +printf '#!/bin/sh\necho "1.2.0"\n' > "${FAKE_BIN_DIR}/context" +chmod +x "${FAKE_BIN_DIR}/context" +exit 0 +SH +chmod +x "$FAKE_BIN2/npm" +export FAKE_BIN_DIR="$FAKE_BIN" + +OUT="$(PATH="$FAKE_BIN2:$FAKE_BIN:$PATH" bash "$INSTALL_SCRIPT" "1.2.0" 2>&1)" +if echo "$OUT" | grep -q "Done:"; then + pass "installs when context absent" +else + fail "expected 'Done:' — got: $OUT" +fi + +echo "" +echo "--- install: version mismatch after npm — exits non-zero ---" + +# npm runs but context still reports wrong version +printf '#!/bin/sh\necho "1.1.0"\n' > "$FAKE_BIN/context"; chmod +x "$FAKE_BIN/context" +printf '#!/bin/sh\necho "npm ok"; exit 0\n' > "$FAKE_BIN/npm"; chmod +x "$FAKE_BIN/npm" +if PATH="$FAKE_BIN:$PATH" bash "$INSTALL_SCRIPT" "1.2.0" >/dev/null 2>&1; then + fail "expected non-zero exit when installed version mismatches target" +else + pass "exits non-zero when installed version does not match target" +fi + +echo "" +echo "--- install: upgrade path message ---" + +printf '#!/bin/sh\necho "1.1.0"\n' > "$FAKE_BIN/context"; chmod +x "$FAKE_BIN/context" +cat > "$FAKE_BIN/npm" <<'SH' +#!/bin/sh +printf '#!/bin/sh\necho "1.2.0"\n' > "${FAKE_BIN_DIR}/context" +chmod +x "${FAKE_BIN_DIR}/context" +exit 0 +SH +chmod +x "$FAKE_BIN/npm" + +OUT="$(FAKE_BIN_DIR="$FAKE_BIN" PATH="$FAKE_BIN:$PATH" bash "$INSTALL_SCRIPT" "1.2.0" 2>&1)" +if echo "$OUT" | grep -q "Upgrading"; then + pass "prints 'Upgrading' when bumping from an older version" +else + fail "expected 'Upgrading' — got: $OUT" +fi + +# --- secure-context-config.sh --- + +echo "" +echo "--- secure: skips when config file does not exist ---" + +FAKE_HOME="$(mktemp -d)" +trap 'rm -rf "$FAKE_HOME"' EXIT +OUT="$(HOME="$FAKE_HOME" bash "$SECURE_SCRIPT" 2>&1)" +if echo "$OUT" | grep -q "Skipped"; then + pass "skips when config file absent" +else + fail "expected 'Skipped' — got: $OUT" +fi + +echo "" +echo "--- secure: sets 600 on existing config ---" + +mkdir -p "$FAKE_HOME/.context" +echo '{"servers":[]}' > "$FAKE_HOME/.context/config.json" +chmod 644 "$FAKE_HOME/.context/config.json" + +HOME="$FAKE_HOME" bash "$SECURE_SCRIPT" > /dev/null +PERMS="$(stat -c '%a' "$FAKE_HOME/.context/config.json")" +if [ "$PERMS" = "600" ]; then + pass "config.json set to 600" +else + fail "expected 600, got $PERMS" +fi + +echo "" +echo "--- secure: idempotent on already-600 file ---" + +HOME="$FAKE_HOME" bash "$SECURE_SCRIPT" > /dev/null +PERMS="$(stat -c '%a' "$FAKE_HOME/.context/config.json")" +if [ "$PERMS" = "600" ]; then + pass "idempotent: 600 remains 600 on re-run" +else + fail "expected 600 after second run, got $PERMS" +fi + +echo "" +echo "--- install: npm exits non-zero — script propagates failure ---" + +printf '#!/bin/sh\necho ""\n' > "$FAKE_BIN/context"; chmod +x "$FAKE_BIN/context" +printf '#!/bin/sh\necho "npm error" >&2; exit 1\n' > "$FAKE_BIN/npm"; chmod +x "$FAKE_BIN/npm" +if PATH="$FAKE_BIN:$PATH" bash "$INSTALL_SCRIPT" "1.2.0" >/dev/null 2>&1; then + fail "expected non-zero exit when npm fails" +else + pass "exits non-zero when npm exits with error" +fi + +echo "" +echo "--- install: no arg uses default version 1.2.0 ---" + +printf '#!/bin/sh\necho "1.2.0"\n' > "$FAKE_BIN/context"; chmod +x "$FAKE_BIN/context" +printf '#!/bin/sh\necho "npm called unexpectedly" >&2; exit 1\n' > "$FAKE_BIN/npm"; chmod +x "$FAKE_BIN/npm" +OUT="$(PATH="$FAKE_BIN:$PATH" bash "$INSTALL_SCRIPT" 2>&1)" +if echo "$OUT" | grep -q "nothing to do"; then + pass "default version 1.2.0 used when no arg given" +else + fail "expected default version skip — got: $OUT" +fi + +echo "" +echo "--- install: fresh install prints 'Installing' not 'Upgrading' ---" + +printf '#!/bin/sh\necho ""\n' > "$FAKE_BIN/context"; chmod +x "$FAKE_BIN/context" +FAKE_BIN3="$(mktemp -d)" +trap 'rm -rf "$FAKE_BIN3"' EXIT +cat > "$FAKE_BIN3/npm" <<'SH' +#!/bin/sh +printf '#!/bin/sh\necho "1.2.0"\n' > "${FAKE_BIN_DIR}/context" +chmod +x "${FAKE_BIN_DIR}/context" +exit 0 +SH +chmod +x "$FAKE_BIN3/npm" +OUT="$(FAKE_BIN_DIR="$FAKE_BIN" PATH="$FAKE_BIN3:$FAKE_BIN:$PATH" bash "$INSTALL_SCRIPT" "1.2.0" 2>&1)" +if echo "$OUT" | grep -q "Installing"; then + pass "fresh install prints 'Installing'" +else + fail "expected 'Installing' on fresh install — got: $OUT" +fi +if echo "$OUT" | grep -q "Upgrading"; then + fail "fresh install should not print 'Upgrading'" +else + pass "fresh install does not print 'Upgrading'" +fi + +echo "" +echo "--- install: context --version with v-prefix is parsed correctly ---" + +printf '#!/bin/sh\necho "v1.2.0"\n' > "$FAKE_BIN/context"; chmod +x "$FAKE_BIN/context" +printf '#!/bin/sh\necho "npm called unexpectedly" >&2; exit 1\n' > "$FAKE_BIN/npm"; chmod +x "$FAKE_BIN/npm" +OUT="$(PATH="$FAKE_BIN:$PATH" bash "$INSTALL_SCRIPT" "1.2.0" 2>&1)" +if echo "$OUT" | grep -q "nothing to do"; then + pass "v-prefixed version output parsed correctly as match" +else + fail "expected 'nothing to do' for v-prefixed version — got: $OUT" +fi + +echo "" +echo "--- secure: downgrades world-writable (777) permissions to 600 ---" + +FAKE_HOME2="$(mktemp -d)" +trap 'rm -rf "$FAKE_HOME2"' EXIT +mkdir -p "$FAKE_HOME2/.context" +echo '{"servers":[]}' > "$FAKE_HOME2/.context/config.json" +chmod 777 "$FAKE_HOME2/.context/config.json" +HOME="$FAKE_HOME2" bash "$SECURE_SCRIPT" > /dev/null +PERMS="$(stat -c '%a' "$FAKE_HOME2/.context/config.json")" +if [ "$PERMS" = "600" ]; then + pass "777 permissions correctly downgraded to 600" +else + fail "expected 600 from 777, got $PERMS" +fi + +echo "" +echo "Results: $PASS passed, $FAIL failed" +[[ $FAIL -eq 0 ]]