From 244bae13d7580eaf6956a919094df9ff0e3c97e1 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 28 Jun 2026 17:32:56 +0000 Subject: [PATCH 1/2] docs: add Cursor Cloud setup notes to AGENTS.md Co-authored-by: hxddh --- AGENTS.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 7770f2e..409857f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -50,3 +50,31 @@ Every skill should keep: - references with `Read when:` guidance. Use `docs/skill-quality-guide.md` and `docs/skill-taxonomy.md` as the source of truth for quality expectations. + +## Cursor Cloud specific instructions + +This is a CLI/skill-pack product — there is no long-running server or GUI. "Running +the app" means the `storageops` CLI plus the offline quality gates. Standard +commands live in the `## Validation Commands` section above, `Makefile`, and +`README.md`; the notes below are only the non-obvious cloud gotchas. + +- **Console scripts are in `~/.local/bin`.** The update script installs with + `pip install -e '.[dev]'`, which puts `storageops` and `pytest` in + `~/.local/bin`. That dir is added to `PATH` via `~/.bashrc`; if a command says + `storageops: not found`, run `export PATH="$HOME/.local/bin:$PATH"`. +- **Two Node versions exist; the default is too old for Pi.** `node` resolves to + `/exec-daemon/node` (v22.14.0), which is **below** Pi's required 22.19+. To run + `storageops install` or launch Pi, put the nvm Node first: + `export PATH="$HOME/.nvm/versions/node/v22.22.2/bin:$PATH"` (or `nvm use 22`). + The extension tests (`make extension-tests`) use `node --experimental-strip-types` + and work on either Node, so plain `make validate-full` is fine without switching. +- **Pi, httpmon, and skills are already deployed** to `~/.storageops/` (via + `storageops install`, persisted in the snapshot). `storageops doctor` should + report everything `ok` except the API key. If `~/.storageops` is ever missing, + re-run `storageops install` with the nvm Node on `PATH` (needs network for the + npm Pi install). +- **Offline vs. live.** All gates and `storageops eval --baselines` / + `storageops eval --list` run fully offline and need no key. A **live diagnosis** + (`storageops --print '...'`) and `storageops smoke` require a model provider key + (`ANTHROPIC_API_KEY` / `DEEPSEEK_API_KEY` / `OPENAI_API_KEY`, etc.), which is not + configured in this environment. From b1a5f823087181ae6529ad91c759ef0e355e3f37 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 29 Jun 2026 17:29:49 +0000 Subject: [PATCH 2/2] feat(dev): add ci-local, dev_setup, and doctor Node PATH hints - make ci-local mirrors CI validate job; validate-full is an alias - make dev runs scripts/dev_setup.sh (venv with --user fallback, nvm Node) - make package-check builds wheel and runs package_check.py locally - storageops doctor suggests nvm PATH when Node < 22.19 (node_path_hint in JSON) - Update CONTRIBUTING, README, AGENTS, release docs and CHANGELOG Co-authored-by: hxddh --- AGENTS.md | 16 +++-- CHANGELOG.md | 11 +++ CONTRIBUTING.md | 33 +++++++-- Makefile | 39 ++++++++--- README.md | 10 +-- docs/release.md | 11 +-- scripts/dev_setup.sh | 116 +++++++++++++++++++++++++++++++ storageops_cli/__init__.py | 51 +++++++++++++- tests/test_cli_productization.py | 32 ++++++++- 9 files changed, 284 insertions(+), 35 deletions(-) create mode 100755 scripts/dev_setup.sh diff --git a/AGENTS.md b/AGENTS.md index 409857f..89df1a1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -29,14 +29,16 @@ Packaging is the most fragile part of this repo. `storageops_cli.__init__._packa ```bash make validate # fast skill/extension/doc gates (greps the extension only) -make validate-full # + pytest, extension behavioral tests, size/routing gates +make ci-local # mirror CI validate job (offline; run before PR) +make validate-full # alias for ci-local +make dev # one-shot dev setup (venv, Node, storageops install) ``` `make validate` does not exercise the TypeScript extension — the routing, provider, and trace logic is only covered by the extension behavioral tests, so -run `make validate-full` (or `make extension-tests`) when touching -`storageops_cli/extensions/storageops.ts`. `package_check.py`, `install-smoke`, -and `diagnosis-smoke` run in CI (wheel build / network) — see `docs/release.md`. +run `make ci-local` (or `make extension-tests`) when touching +`storageops_cli/extensions/storageops.ts`. `make package-check`, `install-smoke`, +and `diagnosis-smoke` need a wheel build or network/model key — see `docs/release.md`. ## Editing Skills @@ -73,8 +75,10 @@ commands live in the `## Validation Commands` section above, `Makefile`, and report everything `ok` except the API key. If `~/.storageops` is ever missing, re-run `storageops install` with the nvm Node on `PATH` (needs network for the npm Pi install). -- **Offline vs. live.** All gates and `storageops eval --baselines` / - `storageops eval --list` run fully offline and need no key. A **live diagnosis** +- **Offline vs. live.** All gates and `make ci-local` / `storageops eval --baselines` + run fully offline and need no key. A **live diagnosis** (`storageops --print '...'`) and `storageops smoke` require a model provider key (`ANTHROPIC_API_KEY` / `DEEPSEEK_API_KEY` / `OPENAI_API_KEY`, etc.), which is not configured in this environment. +- **PR gate.** Run `make ci-local` before opening a PR; it mirrors the CI `validate` + job. Use `make dev` for first-time setup (`scripts/dev_setup.sh`). diff --git a/CHANGELOG.md b/CHANGELOG.md index 97402ab..4ad4ee1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## Unreleased — Dev ergonomics (ci-local, dev_setup, doctor Node hints) + +- **`make ci-local`** mirrors the CI `validate` job offline (provider/contract/coverage + gates, golden-case validation, pytest, extension tests, baseline eval). `make validate-full` + is now an alias. +- **`make dev` / `scripts/dev_setup.sh`** one-shot setup: venv, editable install, Node + >= 22.19 (nvm fallback), `storageops install --force`, and `storageops doctor`. +- **`make package-check`** builds a wheel and runs `package_check.py` locally. +- **`storageops doctor`** suggests an nvm Node `PATH` prefix when the active Node is + < 22.19 (`node_path_hint` in `--json` output). + ## 2026-06-21 — v0.7.0: Unify & Complete (major iteration) A major iteration that closes the last structural gaps rather than adding features. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c6d1b8d..cb27ae6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,9 +7,19 @@ StorageOps is mostly a skill pack plus a thin installer. Good contributions keep ```bash git clone https://github.com/hxddh/storageops.git cd storageops +make dev +``` + +`make dev` runs `scripts/dev_setup.sh`: creates `.venv`, installs `pip install -e '.[dev]'`, +ensures Node.js >= 22.19 (prefers nvm when the default `node` is too old), runs +`storageops install --force`, and prints `storageops doctor`. + +Manual setup (equivalent): + +```bash python3 -m venv .venv .venv/bin/python -m pip install -e '.[dev]' -storageops install --force +storageops install --force # needs Node >= 22.19 and network for Pi/npm ``` ## Common Changes @@ -76,7 +86,16 @@ Edit `storageops_cli/__init__.py`. Be careful with: ## Validation -Run these from the repository root: +Run these from the repository root before opening a PR: + +```bash +make ci-local # mirrors CI validate job (offline) +make package-check # wheel build + package_check.py (needs network once) +``` + +`make ci-local` runs skill gates, provider/contract/coverage checks, golden-case +validation, pytest, extension tests, size/routing gates, and baseline eval. +Individual scripts (for targeted debugging): ```bash python3 scripts/skill_integrity_check.py @@ -88,11 +107,17 @@ python3 skills/storageops-eval-golden-cases/scripts/eval_all.py \ --cases skills/storageops-eval-golden-cases/cases \ --outputs skills/storageops-eval-golden-cases/baseline-outputs \ --only-with-outputs -python3 scripts/package_check.py -make validate .venv/bin/python -m pytest ``` +Optional live diagnosis smoke (needs a model provider API key; CI uses +`STORAGEOPS_MODEL_KEY` — see `.github/workflows/ci.yml` `diagnosis-smoke` job): + +```bash +storageops configure --provider deepseek --model deepseek-v4-pro --api-key +storageops smoke --provider deepseek --model deepseek-v4-pro +``` + ## Pull Request Checklist - The change is scoped and documented. diff --git a/Makefile b/Makefile index 4ecb080..b16038f 100644 --- a/Makefile +++ b/Makefile @@ -1,17 +1,20 @@ -.PHONY: help validate validate-full extension-tests test install +.PHONY: help validate validate-full ci-local extension-tests test install dev package-check help: @echo "StorageOps — Pi Coding Agent extension + skill pack" @echo "" @echo "Usage:" @echo " make validate Fast skill/extension/doc gates (no tests run)" + @echo " make ci-local Mirror CI validate job (offline; run before PR)" + @echo " make validate-full Alias for ci-local" @echo " make extension-tests Run the TypeScript extension behavioral tests" - @echo " make validate-full Everything runnable offline: validate + pytest + extension tests + size/routing gates" @echo " make test Run pytest, extension tests, and validation" - @echo " make install Install thin CLI shim" + @echo " make install Install thin CLI shim (pip install -e .)" + @echo " make dev One-shot dev setup (venv, Node, storageops install)" + @echo " make package-check Build wheel and run package_check.py (needs network once)" @echo "" - @echo " Note: package_check.py, install-smoke, and diagnosis-smoke run in CI" - @echo " (they need a wheel build / network) — see docs/release.md." + @echo " Note: install-smoke and diagnosis-smoke run in CI only" + @echo " (wheel install / model key) — see docs/release.md." validate: @echo "=== Validating skills ===" @@ -30,14 +33,23 @@ validate: extension-tests: @bash scripts/run_extension_tests.sh -# Everything that can run offline, mirroring the deterministic CI gates. Grepping -# the extension is not enough — the routing/provider/trace logic lives in -# TypeScript and is only covered by extension-tests. -validate-full: validate +# Mirrors .github/workflows/ci.yml validate job (everything offline). +ci-local: validate + python3 scripts/provider_scope_check.py + python3 scripts/contract_check.py + python3 scripts/coverage_check.py + python3 skills/storageops-eval-golden-cases/scripts/golden_case_validator.py \ + skills/storageops-eval-golden-cases/cases python3 -m pytest $(MAKE) extension-tests python3 scripts/repo_size_gate.py python3 scripts/routing_contract_check.py + python3 skills/storageops-eval-golden-cases/scripts/eval_all.py \ + --cases skills/storageops-eval-golden-cases/cases \ + --outputs skills/storageops-eval-golden-cases/baseline-outputs \ + --only-with-outputs + +validate-full: ci-local test: python3 -m pytest @@ -46,3 +58,12 @@ test: install: pip install -e . + +dev: + @bash scripts/dev_setup.sh + +package-check: + python3 -m pip install --upgrade pip build + python3 scripts/prepare_httpmon_vendor.py + python3 -m build --wheel + python3 scripts/package_check.py diff --git a/README.md b/README.md index 356157d..938ff8b 100644 --- a/README.md +++ b/README.md @@ -184,14 +184,16 @@ Run quality gates from a checkout: ```bash make validate # fast skill/extension/doc gates (greps the extension) -make validate-full # + pytest, extension behavioral tests, size/routing gates +make ci-local # mirror CI validate job (offline; run before PR) +make validate-full # alias for ci-local +make dev # one-shot dev setup (venv, Node, storageops install) ``` `make validate` only *greps* the TypeScript extension; the routing, provider, and -trace behavior is covered by the extension tests, so run `make validate-full` +trace behavior is covered by the extension tests, so run `make ci-local` (or `make test`) before changing `storageops_cli/extensions/storageops.ts`. -`package_check.py`, `install-smoke`, and `diagnosis-smoke` run in CI (they need a -wheel build or network) — see [Release](docs/release.md). +`make package-check`, `install-smoke`, and `diagnosis-smoke` need a wheel build +or network/model key — see [Release](docs/release.md). ## Documentation Map diff --git a/docs/release.md b/docs/release.md index 73a607c..229dc71 100644 --- a/docs/release.md +++ b/docs/release.md @@ -26,15 +26,8 @@ Trusted Publisher. 2. Run the local quality gates: ```bash - .venv/bin/python -m pytest - make validate - .venv/bin/python scripts/package_check.py - python3 scripts/repo_size_gate.py - python3 scripts/routing_contract_check.py - python3 skills/storageops-eval-golden-cases/scripts/eval_all.py \ - --cases skills/storageops-eval-golden-cases/cases \ - --outputs skills/storageops-eval-golden-cases/baseline-outputs \ - --only-with-outputs + make ci-local + make package-check # optional; wheel build + package_check.py ``` 3. Merge the release PR to `main`. diff --git a/scripts/dev_setup.sh b/scripts/dev_setup.sh new file mode 100755 index 0000000..366f255 --- /dev/null +++ b/scripts/dev_setup.sh @@ -0,0 +1,116 @@ +#!/usr/bin/env bash +# One-shot developer setup: Python editable install, Node >= 22.19 on PATH, +# storageops install, and a doctor readiness check. +set -euo pipefail +cd "$(dirname "$0")/.." + +USE_VENV=1 +SKIP_INSTALL=0 +while [[ $# -gt 0 ]]; do + case "$1" in + --user) + USE_VENV=0 + shift + ;; + --skip-storageops-install) + SKIP_INSTALL=1 + shift + ;; + -h|--help) + echo "Usage: bash scripts/dev_setup.sh [--user] [--skip-storageops-install]" + echo " --user pip install -e '.[dev]' to user site (~/.local/bin)" + echo " --skip-storageops-install skip storageops install --force (offline only)" + exit 0 + ;; + *) + echo "unknown argument: $1" >&2 + exit 2 + ;; + esac +done + +min_node_major=22 +min_node_minor=19 + +node_triple() { + node --version 2>/dev/null | sed -n 's/^v\?\([0-9]*\)\.\([0-9]*\)\.\([0-9]*\).*/\1 \2 \3/p' +} + +node_ok() { + read -r major minor patch <<< "$(node_triple)" || return 1 + [[ -n "${major:-}" ]] || return 1 + if (( major > min_node_major )); then return 0; fi + if (( major == min_node_major && minor >= min_node_minor )); then return 0; fi + return 1 +} + +prepend_nvm_node() { + local nvm_root="${NVM_DIR:-$HOME/.nvm}/versions/node" + [[ -d "$nvm_root" ]] || return 1 + local best="" + local best_ver="" + for dir in "$nvm_root"/*; do + [[ -d "$dir/bin" && -x "$dir/bin/node" ]] || continue + local ver="${dir##*/}" + ver="${ver#v}" + IFS=. read -r major minor patch <<< "$ver" + [[ -n "${major:-}" ]] || continue + if (( major > min_node_major || (major == min_node_major && minor >= min_node_minor) )); then + if [[ -z "$best_ver" || "$ver" > "$best_ver" ]]; then + best="$dir/bin" + best_ver="$ver" + fi + fi + done + if [[ -n "$best" ]]; then + export PATH="$best:$PATH" + return 0 + fi + return 1 +} + +echo "=== Python dev install ===" +if [[ "$USE_VENV" == 1 ]]; then + if ! python3 -m venv .venv 2>/dev/null; then + echo "[warn] python3 -m venv failed (install python3-venv); falling back to --user install" + USE_VENV=0 + fi +fi +if [[ "$USE_VENV" == 1 ]]; then + # shellcheck disable=SC1091 + source .venv/bin/activate + python -m pip install -U pip + python -m pip install -e '.[dev]' + export PATH="$(pwd)/.venv/bin:$PATH" +else + python3 -m pip install -U pip + python3 -m pip install -e '.[dev]' + export PATH="$HOME/.local/bin:$PATH" +fi + +echo "=== Node.js (need >= ${min_node_major}.${min_node_minor}) ===" +if ! node_ok; then + if prepend_nvm_node && node_ok; then + echo "[ok] using nvm Node $(node --version) on PATH" + else + echo "[error] Node $(node --version 2>/dev/null || echo 'not found') is too old for Pi Coding Agent." >&2 + echo " Install Node >= ${min_node_major}.${min_node_minor} (e.g. nvm install 22) and re-run." >&2 + exit 1 + fi +else + echo "[ok] Node $(node --version)" +fi + +if [[ "$SKIP_INSTALL" != 1 ]]; then + echo "=== storageops install --force ===" + storageops install --force +else + echo "=== skipping storageops install (--skip-storageops-install) ===" +fi + +echo "=== storageops doctor ===" +storageops doctor || true + +echo +echo "Dev setup complete. Before opening a PR, run:" +echo " make ci-local" diff --git a/storageops_cli/__init__.py b/storageops_cli/__init__.py index 7a8bc92..b0a3c7b 100644 --- a/storageops_cli/__init__.py +++ b/storageops_cli/__init__.py @@ -291,6 +291,52 @@ def _node_too_old() -> tuple[bool, str]: return nv < MIN_NODE_VERSION, ".".join(str(p) for p in nv) +def _suggest_node_path() -> str | None: + """Return a PATH prefix for Node >= MIN_NODE_VERSION when discoverable (e.g. nvm).""" + nvm_root = Path.home() / ".nvm" / "versions" / "node" + if not nvm_root.is_dir(): + return None + candidates: list[tuple[tuple[int, int, int], Path]] = [] + for entry in nvm_root.iterdir(): + if not entry.is_dir(): + continue + node_bin = entry / "bin" / "node" + if not node_bin.is_file(): + continue + ver = _parse_version_triple(entry.name) + if ver and ver >= MIN_NODE_VERSION: + candidates.append((ver, entry / "bin")) + if not candidates: + return None + candidates.sort(key=lambda item: item[0], reverse=True) + return str(candidates[0][1]) + + +def _node_doctor_detail(s: dict) -> str: + """Human-readable Node row for doctor, with PATH hint when too old.""" + nv = s["node_triple"] + label = ".".join(str(p) for p in nv) if nv else "not found" + if s["node_ok"]: + return label + min_node = ".".join(str(p) for p in MIN_NODE_VERSION) + detail = f"{label} -- need >= {min_node}" + hint = _suggest_node_path() + if hint: + detail += f"; try: export PATH=\"{hint}:$PATH\"" + return detail + + +def _node_readiness_action(s: dict) -> str: + """Next action when Node or Pi is not ready.""" + if not s["node_ok"]: + hint = _suggest_node_path() + if hint: + return f'export PATH="{hint}:$PATH" && storageops install --force' + min_node = ".".join(str(p) for p in MIN_NODE_VERSION) + return f"upgrade Node to >= {min_node}, then run storageops install --force" + return "fix Node/Pi, then run storageops install --force" + + def _ensure_pi() -> str: """ Ensure Pi Coding Agent is available; return the pi executable path. @@ -1002,7 +1048,7 @@ def _doctor_readiness(s: dict) -> tuple[bool, str]: if not s["key_source"]: return False, "storageops configure --provider deepseek --model deepseek-v4-pro --api-key" if not s["pi_ok"] or not s["node_ok"]: - return False, "fix Node/Pi, then run storageops install --force" + return False, _node_readiness_action(s) return True, "storageops --print 'hello'" @@ -1017,6 +1063,7 @@ def _doctor_report(s: dict, ready: bool, next_action: str) -> dict: "pi_ok": s["pi_ok"], "node_version": ".".join(str(p) for p in nv) if nv else None, "node_ok": s["node_ok"], + "node_path_hint": _suggest_node_path(), "install_mode": "independent" if s["independent"] else "merged" if s["merged"] else "not_installed", "agent_dir": str(s["active_agent"]), "skill_count": s["skill_count"], @@ -1053,7 +1100,7 @@ def cmd_doctor(as_json: bool = False) -> int: _doctor_row("PyPI", "warn", f"newer version available: {latest}") else: _doctor_row("PyPI", "ok", latest or "version check unavailable") - _doctor_row("Node", "ok" if s["node_ok"] else "warn", node_label) + _doctor_row("Node", "ok" if s["node_ok"] else "warn", _node_doctor_detail(s)) pi_detail = f"{s['pi_ver']} ({s['pi_path']})" pi_latest = s["pi_latest"] if s["pi_ok"] and pi_latest and _is_newer_version(pi_latest, s["pi_ver"]): diff --git a/tests/test_cli_productization.py b/tests/test_cli_productization.py index c7ed2ac..a77d718 100644 --- a/tests/test_cli_productization.py +++ b/tests/test_cli_productization.py @@ -2,7 +2,12 @@ import subprocess import storageops_cli -from storageops_cli import _default_model_label +from storageops_cli import ( + _default_model_label, + _node_doctor_detail, + _node_readiness_action, + _suggest_node_path, +) def _clear_provider_env(monkeypatch): @@ -147,6 +152,31 @@ def test_doctor_exit_code_reflects_not_ready(tmp_path, monkeypatch, capsys): assert "Next: storageops configure" in out +def test_suggest_node_path_picks_newest_nvm(tmp_path, monkeypatch): + nvm_root = tmp_path / ".nvm" / "versions" / "node" + for ver in ("v22.19.0", "v22.22.2", "v20.10.0"): + bindir = nvm_root / ver / "bin" + bindir.mkdir(parents=True) + (bindir / "node").write_text("") + monkeypatch.setattr(storageops_cli.Path, "home", lambda: tmp_path) + assert _suggest_node_path() == str(nvm_root / "v22.22.2" / "bin") + + +def test_doctor_node_detail_includes_path_hint(monkeypatch): + monkeypatch.setattr(storageops_cli, "_suggest_node_path", lambda: "/opt/node22/bin") + s = {"node_triple": (22, 14, 0), "node_ok": False} + detail = _node_doctor_detail(s) + assert "22.14.0" in detail + assert "22.19" in detail + assert 'export PATH="/opt/node22/bin:$PATH"' in detail + + +def test_node_readiness_action_prefers_nvm_hint(monkeypatch): + monkeypatch.setattr(storageops_cli, "_suggest_node_path", lambda: "/opt/node22/bin") + action = _node_readiness_action({"node_ok": False}) + assert action.startswith('export PATH="/opt/node22/bin:$PATH"') + + def test_smoke_runs_pi_with_selected_agent_and_model(tmp_path, monkeypatch, capsys): root = tmp_path / ".storageops" agent_dir = _installed_agent(root)