diff --git a/backend/package-lock.json b/backend/package-lock.json index 6bb26af..ce8542c 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1,12 +1,12 @@ { "name": "mikrotik-manager-backend", - "version": "0.17.4-beta", + "version": "0.18.1-beta", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mikrotik-manager-backend", - "version": "0.17.4-beta", + "version": "0.18.1-beta", "dependencies": { "@influxdata/influxdb-client": "^1.33.2", "@influxdata/influxdb-client-apis": "^1.35.0", @@ -3095,9 +3095,9 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz", - "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "dev": true, "license": "MIT", "dependencies": { @@ -4704,9 +4704,9 @@ "license": "MIT" }, "node_modules/glob/node_modules/brace-expansion": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.3.tgz", - "integrity": "sha512-DRdx5neNsG/QXbniLFWi2YmC/68oeOOmKz6zOjVk6ZS1ZLXgLIKqVEc6hWsmkjBbgii0SwaBTcJ5XKj5gzY/4A==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", "dev": true, "license": "MIT", "dependencies": { @@ -5883,9 +5883,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.0.tgz", - "integrity": "sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==", + "version": "3.15.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.1.tgz", + "integrity": "sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag==", "dev": true, "license": "MIT", "dependencies": { @@ -7690,9 +7690,9 @@ "license": "MIT" }, "node_modules/socket.io-parser": { - "version": "4.2.6", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz", - "integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.7.tgz", + "integrity": "sha512-IH/iSeO9T6gz1KkFleGDWkG9N3dl4jXVYUtMhIqH10Md0ttMer8nUNWiP1DKuNrybD2xBrixLJdCC9J6ECoYkg==", "license": "MIT", "dependencies": { "@socket.io/component-emitter": "~3.1.0", @@ -8079,9 +8079,9 @@ "license": "MIT" }, "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "1.1.17", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.17.tgz", - "integrity": "sha512-w+aeW/mkgM4PyRMOJCgi3fOrTm5Q8QY1OSfn2TO2iuDj3ezIHqejmuxbjfPrqUkgqRew1iqkyAn0tr0ZwHD9+w==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", "dependencies": { diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 6e735f0..98512e3 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "mikrotik-manager-frontend", - "version": "0.17.4-beta", + "version": "0.18.1-beta", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mikrotik-manager-frontend", - "version": "0.17.4-beta", + "version": "0.18.1-beta", "dependencies": { "@tanstack/react-query": "^5.101.0", "@types/leaflet": "^1.9.21", @@ -1348,9 +1348,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz", - "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "dev": true, "license": "MIT", "dependencies": { @@ -2006,9 +2006,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.17", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.17.tgz", - "integrity": "sha512-w+aeW/mkgM4PyRMOJCgi3fOrTm5Q8QY1OSfn2TO2iuDj3ezIHqejmuxbjfPrqUkgqRew1iqkyAn0tr0ZwHD9+w==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", "dependencies": { @@ -4249,9 +4249,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", - "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", "dev": true, "funding": [ { @@ -4813,9 +4813,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.16", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", - "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "dev": true, "funding": [ { @@ -5960,9 +5960,9 @@ } }, "node_modules/socket.io-parser": { - "version": "4.2.6", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz", - "integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.7.tgz", + "integrity": "sha512-IH/iSeO9T6gz1KkFleGDWkG9N3dl4jXVYUtMhIqH10Md0ttMer8nUNWiP1DKuNrybD2xBrixLJdCC9J6ECoYkg==", "license": "MIT", "dependencies": { "@socket.io/component-emitter": "~3.1.0", diff --git a/scripts/ci-preflight.sh b/scripts/ci-preflight.sh new file mode 100755 index 0000000..9032307 --- /dev/null +++ b/scripts/ci-preflight.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# Run the same gates GitHub Actions runs, locally, before pushing. +# +# Mirrors .github/workflows/ci.yml exactly (frontend lint/type-check/build, +# backend lint/type-check/build, prod-dependency audits) and additionally runs +# the backend Jest suite, which CI does not currently cover. +# +# Usage: scripts/ci-preflight.sh [--skip-audit] +# Exit: 0 = everything CI checks would pass, 1 = at least one gate failed. +# +# Runs every gate rather than failing fast, so one invocation surfaces all +# problems instead of dribbling them out one push at a time. + +set -uo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +SKIP_AUDIT=0 +[[ "${1:-}" == "--skip-audit" ]] && SKIP_AUDIT=1 + +FAILED=() +run() { + local label="$1" dir="$2"; shift 2 + printf ' %-34s' "$label" + if (cd "$ROOT/$dir" && "$@") >/tmp/ci-preflight-out 2>&1; then + echo "ok" + else + echo "FAIL" + FAILED+=("$label") + # Keep a trimmed excerpt so the caller can see why without re-running. + { + echo "----- $label -----" + tail -25 /tmp/ci-preflight-out + echo + } >>/tmp/ci-preflight-failures + fi +} + +: >/tmp/ci-preflight-failures +echo "Running CI gates locally (mirrors .github/workflows/ci.yml)" + +run "backend: lint" backend npm run lint +run "backend: type-check" backend npx tsc --noEmit +run "backend: build" backend npm run build +run "backend: tests" backend npx jest --silent +run "frontend: lint" frontend npm run lint +run "frontend: type-check" frontend npx tsc --noEmit +run "frontend: build" frontend npm run build + +if [[ $SKIP_AUDIT -eq 0 ]]; then + run "frontend: audit (prod)" frontend \ + npx --yes audit-ci@^7 --high --skip-dev --allowlist GHSA-qwww-vcr4-c8h2 + run "backend: audit (prod)" backend \ + npx --yes audit-ci@^7 --high --skip-dev +fi + +if [[ ${#FAILED[@]} -eq 0 ]]; then + echo "All CI gates passed." + exit 0 +fi + +echo +echo "FAILED: ${FAILED[*]}" +echo "Details in /tmp/ci-preflight-failures" +exit 1 diff --git a/scripts/claude-push-gate.sh b/scripts/claude-push-gate.sh new file mode 100755 index 0000000..584cedf --- /dev/null +++ b/scripts/claude-push-gate.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# Claude Code PreToolUse(Bash) hook: refuse `git push` until the local CI gates pass. +# +# Reads the hook payload on stdin. Non-push commands exit immediately, so the +# overhead on ordinary Bash calls is one short script invocation. +# +# On failure it emits a PreToolUse deny decision, which blocks the push and hands +# the reason back to the model so it can fix the problem instead of retrying. + +set -uo pipefail + +PAYLOAD="$(cat)" +CMD="$(printf '%s' "$PAYLOAD" | jq -r '.tool_input.command // ""' 2>/dev/null)" + +# Only gate actual pushes (covers `git push`, `git -C push`, flags, etc.). +# Deliberately ignores --dry-run. +if ! printf '%s' "$CMD" | grep -Eq '(^|[;&|]|\s)git(\s+-[^;&|]*)?\s+push(\s|$)'; then + exit 0 +fi +if printf '%s' "$CMD" | grep -q -- '--dry-run'; then + exit 0 +fi + +REPO="/Users/rteslow/mikrotik-manager" +[[ -x "$REPO/scripts/ci-preflight.sh" ]] || exit 0 # nothing to enforce + +if OUTPUT="$("$REPO/scripts/ci-preflight.sh" 2>&1)"; then + exit 0 +fi + +DETAIL="$(tail -60 /tmp/ci-preflight-failures 2>/dev/null)" +python3 - "$OUTPUT" "$DETAIL" <<'PY' +import json, sys +summary, detail = sys.argv[1], sys.argv[2] +reason = ( + "Push blocked: the local CI gates failed, so this would fail on GitHub too.\n\n" + f"{summary.strip()}\n\n{detail.strip()}\n\n" + "Fix these, re-run scripts/ci-preflight.sh until it passes, then push." +) +print(json.dumps({ + "hookSpecificOutput": { + "hookEventName": "PreToolUse", + "permissionDecision": "deny", + "permissionDecisionReason": reason, + } +})) +PY +exit 0