mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-06-07 23:03:54 +02:00
v0.9.6: InfoNet hashchain, Wormhole gate encryption, mesh reputation, 16 community contributors
Gate messages now propagate via the Infonet hashchain as encrypted blobs — every node syncs them through normal chain sync while only Gate members with MLS keys can decrypt. Added mesh reputation system, peer push workers, voluntary Wormhole opt-in for node participation, fork recovery, killwormhole scripts, obfuscated terminology, and hardened the self-updater to protect encryption keys and chain state during updates. New features: Shodan search, train tracking, Sentinel Hub imagery, 8 new intelligence layers, CCTV expansion to 11,000+ cameras across 6 countries, Mesh Terminal CLI, prediction markets, desktop-shell scaffold, and comprehensive mesh test suite (215 frontend + backend tests passing). Community contributors: @wa1id, @AlborzNazari, @adust09, @Xpirix, @imqdcr, @csysp, @suranyami, @chr0n1x, @johan-martensson, @singularfailure, @smithbh, @OrfeoTerkuci, @deuza, @tm-const, @Elhard1, @ttulttul
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
BACKEND_DIR = ROOT / "backend"
|
||||
|
||||
if str(BACKEND_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(BACKEND_DIR))
|
||||
|
||||
from services.mesh.mesh_bootstrap_manifest import ( # noqa: E402
|
||||
bootstrap_signer_public_key_b64,
|
||||
generate_bootstrap_signer,
|
||||
write_signed_bootstrap_manifest,
|
||||
)
|
||||
|
||||
|
||||
def _load_peers(args: argparse.Namespace) -> list[dict]:
|
||||
peers: list[dict] = []
|
||||
if args.peers_file:
|
||||
raw = json.loads(Path(args.peers_file).read_text(encoding="utf-8"))
|
||||
if not isinstance(raw, list):
|
||||
raise ValueError("peers file must be a JSON array")
|
||||
for entry in raw:
|
||||
if not isinstance(entry, dict):
|
||||
raise ValueError("peers file entries must be objects")
|
||||
peers.append(dict(entry))
|
||||
for peer_arg in args.peer or []:
|
||||
parts = [part.strip() for part in str(peer_arg).split(",", 3)]
|
||||
if len(parts) < 3:
|
||||
raise ValueError("peer entries must look like url,transport,role[,label]")
|
||||
peer_url, transport, role = parts[:3]
|
||||
label = parts[3] if len(parts) > 3 else ""
|
||||
peers.append(
|
||||
{
|
||||
"peer_url": peer_url,
|
||||
"transport": transport,
|
||||
"role": role,
|
||||
"label": label,
|
||||
}
|
||||
)
|
||||
if not peers:
|
||||
raise ValueError("at least one peer is required")
|
||||
return peers
|
||||
|
||||
|
||||
def cmd_generate_keypair(_args: argparse.Namespace) -> int:
|
||||
signer = generate_bootstrap_signer()
|
||||
print(json.dumps(signer, indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_sign(args: argparse.Namespace) -> int:
|
||||
peers = _load_peers(args)
|
||||
manifest = write_signed_bootstrap_manifest(
|
||||
args.output,
|
||||
signer_id=args.signer_id,
|
||||
signer_private_key_b64=args.private_key_b64,
|
||||
peers=peers,
|
||||
valid_for_hours=int(args.valid_hours),
|
||||
)
|
||||
print(f"Wrote signed bootstrap manifest to {Path(args.output).resolve()}")
|
||||
print(f"signer_id={manifest.signer_id}")
|
||||
print(f"valid_until={manifest.valid_until}")
|
||||
print(f"peer_count={len(manifest.peers)}")
|
||||
print(f"MESH_BOOTSTRAP_SIGNER_PUBLIC_KEY={bootstrap_signer_public_key_b64(args.private_key_b64)}")
|
||||
return 0
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate and sign Infonet bootstrap manifests for participant nodes."
|
||||
)
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
keygen = subparsers.add_parser("generate-keypair", help="Generate an Ed25519 bootstrap signer keypair")
|
||||
keygen.set_defaults(func=cmd_generate_keypair)
|
||||
|
||||
sign = subparsers.add_parser("sign", help="Sign a bootstrap manifest from peer entries")
|
||||
sign.add_argument("--output", required=True, help="Output path for bootstrap_peers.json")
|
||||
sign.add_argument("--signer-id", required=True, help="Manifest signer identifier")
|
||||
sign.add_argument(
|
||||
"--private-key-b64",
|
||||
required=True,
|
||||
help="Raw Ed25519 private key in base64 returned by generate-keypair",
|
||||
)
|
||||
sign.add_argument(
|
||||
"--peers-file",
|
||||
help="JSON file containing an array of peer objects with peer_url, transport, role, and optional label",
|
||||
)
|
||||
sign.add_argument(
|
||||
"--peer",
|
||||
action="append",
|
||||
help="Inline peer in the form url,transport,role[,label]. May be repeated.",
|
||||
)
|
||||
sign.add_argument(
|
||||
"--valid-hours",
|
||||
type=int,
|
||||
default=168,
|
||||
help="Manifest validity window in hours (default: 168)",
|
||||
)
|
||||
sign.set_defaults(func=cmd_sign)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = build_parser()
|
||||
args = parser.parse_args()
|
||||
return args.func(args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,5 @@
|
||||
param(
|
||||
[string]$Python = "python"
|
||||
)
|
||||
|
||||
& $Python -c "from services.env_check import validate_env; validate_env(strict=False)"
|
||||
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
PYTHON="${PYTHON:-python3}"
|
||||
"$PYTHON" -c "from services.env_check import validate_env; validate_env(strict=False)"
|
||||
@@ -0,0 +1,45 @@
|
||||
from datetime import datetime
|
||||
from services.data_fetcher import get_latest_data
|
||||
from services.fetchers._store import source_timestamps, active_layers, source_freshness
|
||||
from services.fetch_health import get_health_snapshot
|
||||
|
||||
|
||||
def _fmt_ts(ts: str | None) -> str:
|
||||
if not ts:
|
||||
return "-"
|
||||
try:
|
||||
return datetime.fromisoformat(ts).strftime("%Y-%m-%d %H:%M:%S")
|
||||
except Exception:
|
||||
return ts
|
||||
|
||||
|
||||
def main():
|
||||
data = get_latest_data()
|
||||
print("=== Diagnostics ===")
|
||||
print(f"Last updated: {_fmt_ts(data.get('last_updated'))}")
|
||||
print(
|
||||
f"Active layers: {sum(1 for v in active_layers.values() if v)} enabled / {len(active_layers)} total"
|
||||
)
|
||||
|
||||
print("\n--- Source Timestamps ---")
|
||||
for k, v in sorted(source_timestamps.items()):
|
||||
print(f"{k:20} {_fmt_ts(v)}")
|
||||
|
||||
print("\n--- Source Freshness ---")
|
||||
for k, v in sorted(source_freshness.items()):
|
||||
last_ok = _fmt_ts(v.get("last_ok"))
|
||||
last_err = _fmt_ts(v.get("last_error"))
|
||||
print(f"{k:20} ok={last_ok} err={last_err}")
|
||||
|
||||
print("\n--- Fetch Health ---")
|
||||
health = get_health_snapshot()
|
||||
for k, v in sorted(health.items()):
|
||||
print(
|
||||
f"{k:20} ok={v.get('ok_count', 0)} err={v.get('error_count', 0)} "
|
||||
f"last_ok={_fmt_ts(v.get('last_ok'))} last_err={_fmt_ts(v.get('last_error'))} "
|
||||
f"avg_ms={v.get('avg_duration_ms')}"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,138 @@
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
PACKAGE_JSON = ROOT / "frontend" / "package.json"
|
||||
|
||||
|
||||
def _normalize_version(raw: str) -> str:
|
||||
version = str(raw or "").strip()
|
||||
if version.startswith("v"):
|
||||
version = version[1:]
|
||||
parts = version.split(".")
|
||||
if len(parts) != 3 or not all(part.isdigit() for part in parts):
|
||||
raise ValueError("Version must look like X.Y.Z")
|
||||
return version
|
||||
|
||||
|
||||
def _read_package_json() -> dict:
|
||||
return json.loads(PACKAGE_JSON.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def _write_package_json(data: dict) -> None:
|
||||
PACKAGE_JSON.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def current_version() -> str:
|
||||
return str(_read_package_json().get("version") or "").strip()
|
||||
|
||||
|
||||
def set_version(version: str) -> str:
|
||||
normalized = _normalize_version(version)
|
||||
data = _read_package_json()
|
||||
data["version"] = normalized
|
||||
_write_package_json(data)
|
||||
return normalized
|
||||
|
||||
|
||||
def expected_tag(version: str) -> str:
|
||||
return f"v{_normalize_version(version)}"
|
||||
|
||||
|
||||
def expected_asset(version: str) -> str:
|
||||
normalized = _normalize_version(version)
|
||||
return f"ShadowBroker_v{normalized}.zip"
|
||||
|
||||
|
||||
def sha256_file(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as handle:
|
||||
for chunk in iter(lambda: handle.read(1024 * 128), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest().lower()
|
||||
|
||||
|
||||
def cmd_show(_args: argparse.Namespace) -> int:
|
||||
version = current_version()
|
||||
if not version:
|
||||
print("package.json has no version", file=sys.stderr)
|
||||
return 1
|
||||
print(f"package.json version : {version}")
|
||||
print(f"expected git tag : {expected_tag(version)}")
|
||||
print(f"expected zip asset : {expected_asset(version)}")
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_set_version(args: argparse.Namespace) -> int:
|
||||
version = set_version(args.version)
|
||||
print(f"Set frontend/package.json version to {version}")
|
||||
print(f"Next release tag : {expected_tag(version)}")
|
||||
print(f"Next zip asset : {expected_asset(version)}")
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_hash(args: argparse.Namespace) -> int:
|
||||
version = _normalize_version(args.version) if args.version else current_version()
|
||||
if not version:
|
||||
print("No version available; pass --version or set frontend/package.json", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
zip_path = Path(args.zip_path).resolve()
|
||||
if not zip_path.is_file():
|
||||
print(f"ZIP not found: {zip_path}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
digest = sha256_file(zip_path)
|
||||
expected_name = expected_asset(version)
|
||||
asset_matches = zip_path.name == expected_name
|
||||
|
||||
print(f"release version : {version}")
|
||||
print(f"expected git tag : {expected_tag(version)}")
|
||||
print(f"zip path : {zip_path}")
|
||||
print(f"zip name matches : {'yes' if asset_matches else 'no'}")
|
||||
print(f"expected zip asset : {expected_name}")
|
||||
print(f"SHA-256 : {digest}")
|
||||
print("")
|
||||
print("Updater pin:")
|
||||
print(f"MESH_UPDATE_SHA256={digest}")
|
||||
return 0 if asset_matches else 2
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Helper for ShadowBroker release version/tag/asset consistency."
|
||||
)
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
show_parser = subparsers.add_parser("show", help="Show current version, expected tag, and asset")
|
||||
show_parser.set_defaults(func=cmd_show)
|
||||
|
||||
set_version_parser = subparsers.add_parser("set-version", help="Update frontend/package.json version")
|
||||
set_version_parser.add_argument("version", help="Version like 0.9.6")
|
||||
set_version_parser.set_defaults(func=cmd_set_version)
|
||||
|
||||
hash_parser = subparsers.add_parser(
|
||||
"hash", help="Compute SHA-256 for a release ZIP and print the updater pin"
|
||||
)
|
||||
hash_parser.add_argument("zip_path", help="Path to the release ZIP")
|
||||
hash_parser.add_argument(
|
||||
"--version",
|
||||
help="Release version like 0.9.6. Defaults to frontend/package.json version.",
|
||||
)
|
||||
hash_parser.set_defaults(func=cmd_hash)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = build_parser()
|
||||
args = parser.parse_args()
|
||||
return args.func(args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,48 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from services.mesh import mesh_secure_storage
|
||||
from services.mesh.mesh_wormhole_contacts import CONTACTS_FILE
|
||||
from services.mesh.mesh_wormhole_identity import IDENTITY_FILE, _default_identity
|
||||
from services.mesh.mesh_wormhole_persona import PERSONA_FILE, _default_state as _default_persona_state
|
||||
from services.mesh.mesh_wormhole_ratchet import STATE_FILE as RATCHET_FILE
|
||||
|
||||
|
||||
def _load_payloads() -> dict[Path, object]:
|
||||
return {
|
||||
IDENTITY_FILE: mesh_secure_storage.read_secure_json(IDENTITY_FILE, _default_identity),
|
||||
PERSONA_FILE: mesh_secure_storage.read_secure_json(PERSONA_FILE, _default_persona_state),
|
||||
RATCHET_FILE: mesh_secure_storage.read_secure_json(RATCHET_FILE, lambda: {}),
|
||||
CONTACTS_FILE: mesh_secure_storage.read_secure_json(CONTACTS_FILE, lambda: {}),
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
payloads = _load_payloads()
|
||||
|
||||
master_key_file = mesh_secure_storage.MASTER_KEY_FILE
|
||||
backup_key_file = master_key_file.with_suffix(master_key_file.suffix + ".bak")
|
||||
if master_key_file.exists():
|
||||
if backup_key_file.exists():
|
||||
backup_key_file.unlink()
|
||||
master_key_file.replace(backup_key_file)
|
||||
|
||||
for path, payload in payloads.items():
|
||||
mesh_secure_storage.write_secure_json(path, payload)
|
||||
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"ok": True,
|
||||
"rewrapped": [str(path.name) for path in payloads.keys()],
|
||||
"master_key": str(master_key_file),
|
||||
"backup_master_key": str(backup_key_file) if backup_key_file.exists() else "",
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env bash
|
||||
# scan-secrets.sh — Catch keys, secrets, and credentials before they hit git.
|
||||
#
|
||||
# Usage:
|
||||
# ./backend/scripts/scan-secrets.sh # Scan staged files (pre-commit)
|
||||
# ./backend/scripts/scan-secrets.sh --all # Scan entire working tree
|
||||
# ./backend/scripts/scan-secrets.sh --staged # Scan staged files only (default)
|
||||
#
|
||||
# Exit code: 0 = clean, 1 = secrets found
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
RED='\033[0;31m'
|
||||
YELLOW='\033[1;33m'
|
||||
GREEN='\033[0;32m'
|
||||
NC='\033[0m'
|
||||
|
||||
MODE="${1:---staged}"
|
||||
FOUND=0
|
||||
|
||||
# ── Get file list based on mode ─────────────────────────────────────────
|
||||
if [[ "$MODE" == "--all" ]]; then
|
||||
FILELIST=$(mktemp)
|
||||
{ git ls-files 2>/dev/null; git ls-files --others --exclude-standard 2>/dev/null; } > "$FILELIST"
|
||||
echo -e "${YELLOW}Scanning entire working tree...${NC}"
|
||||
else
|
||||
FILELIST=$(mktemp)
|
||||
git diff --cached --name-only --diff-filter=ACMR 2>/dev/null > "$FILELIST" || true
|
||||
if [[ ! -s "$FILELIST" ]]; then
|
||||
echo -e "${GREEN}No staged files to scan.${NC}"
|
||||
rm -f "$FILELIST"
|
||||
exit 0
|
||||
fi
|
||||
echo -e "${YELLOW}Scanning $(wc -l < "$FILELIST" | tr -d ' ') staged files...${NC}"
|
||||
fi
|
||||
|
||||
# ── Check 1: Dangerous file extensions ──────────────────────────────────
|
||||
KEY_EXT='\.key$|\.pem$|\.p12$|\.pfx$|\.jks$|\.keystore$|\.p8$|\.der$'
|
||||
SECRET_EXT='\.secret$|\.secrets$|\.credential$|\.credentials$'
|
||||
|
||||
HITS=$(grep -iE "$KEY_EXT|$SECRET_EXT" "$FILELIST" 2>/dev/null || true)
|
||||
if [[ -n "$HITS" ]]; then
|
||||
echo -e "\n${RED}BLOCKED: Key/secret files detected:${NC}"
|
||||
echo "$HITS" | while read -r f; do echo -e " ${RED}$f${NC}"; done
|
||||
FOUND=1
|
||||
fi
|
||||
|
||||
# ── Check 2: Dangerous filenames ────────────────────────────────────────
|
||||
RISKY='id_rsa|id_ed25519|id_ecdsa|private_key|private\.key|secret_key|master\.key'
|
||||
RISKY+='|serviceaccount|gcloud.*\.json|firebase.*\.json|\.htpasswd'
|
||||
|
||||
HITS=$(grep -iE "$RISKY" "$FILELIST" 2>/dev/null || true)
|
||||
if [[ -n "$HITS" ]]; then
|
||||
echo -e "\n${RED}BLOCKED: Risky filenames detected:${NC}"
|
||||
echo "$HITS" | while read -r f; do echo -e " ${RED}$f${NC}"; done
|
||||
FOUND=1
|
||||
fi
|
||||
|
||||
# ── Check 3: .env files (not .env.example) ──────────────────────────────
|
||||
HITS=$(grep -E '(^|/)\.env(\.[^e].*)?$' "$FILELIST" 2>/dev/null | grep -v '\.example' || true)
|
||||
if [[ -n "$HITS" ]]; then
|
||||
echo -e "\n${RED}BLOCKED: Environment files detected:${NC}"
|
||||
echo "$HITS" | while read -r f; do echo -e " ${RED}$f${NC}"; done
|
||||
FOUND=1
|
||||
fi
|
||||
|
||||
# ── Check 4: _domain_keys directory (project-specific) ──────────────────
|
||||
HITS=$(grep '_domain_keys/' "$FILELIST" 2>/dev/null || true)
|
||||
if [[ -n "$HITS" ]]; then
|
||||
echo -e "\n${RED}BLOCKED: Domain keys directory detected:${NC}"
|
||||
echo "$HITS" | while read -r f; do echo -e " ${RED}$f${NC}"; done
|
||||
FOUND=1
|
||||
fi
|
||||
|
||||
# ── Check 5: Content scan for embedded secrets (single grep pass) ───────
|
||||
# Build one mega-pattern and run grep once across all files (fast!)
|
||||
SECRET_REGEX='PRIVATE KEY-----|'
|
||||
SECRET_REGEX+='ssh-rsa AAAA[0-9A-Za-z+/]|'
|
||||
SECRET_REGEX+='ssh-ed25519 AAAA[0-9A-Za-z+/]|'
|
||||
SECRET_REGEX+='ghp_[0-9a-zA-Z]{36}|' # GitHub PAT
|
||||
SECRET_REGEX+='github_pat_[0-9a-zA-Z]{22}_[0-9a-zA-Z]{59}|' # GitHub fine-grained
|
||||
SECRET_REGEX+='gho_[0-9a-zA-Z]{36}|' # GitHub OAuth
|
||||
SECRET_REGEX+='sk-[0-9a-zA-Z]{48}|' # OpenAI key
|
||||
SECRET_REGEX+='sk-ant-[0-9a-zA-Z-]{90,}|' # Anthropic key
|
||||
SECRET_REGEX+='AKIA[0-9A-Z]{16}|' # AWS access key
|
||||
SECRET_REGEX+='AIzaSy[0-9A-Za-z_-]{33}|' # Google API key
|
||||
SECRET_REGEX+='xox[bpoas]-[0-9a-zA-Z-]+|' # Slack token
|
||||
SECRET_REGEX+='npm_[0-9a-zA-Z]{36}|' # npm token
|
||||
SECRET_REGEX+='pypi-[0-9a-zA-Z-]{50,}' # PyPI token
|
||||
|
||||
# Filter to text-like files only (skip binaries by extension + skip this script)
|
||||
TEXT_FILES=$(grep -ivE '\.(png|jpg|jpeg|gif|ico|svg|woff2?|ttf|eot|pbf|zip|tar|gz|db|sqlite|xlsx|pdf|mp[34]|wav|ogg|webm|webp|avif)$' "$FILELIST" | grep -v 'scan-secrets\.sh$' || true)
|
||||
|
||||
if [[ -n "$TEXT_FILES" ]]; then
|
||||
# Use grep with file list, skip missing/binary, limit output
|
||||
CONTENT_HITS=$(echo "$TEXT_FILES" | xargs grep -lE "$SECRET_REGEX" 2>/dev/null || true)
|
||||
if [[ -n "$CONTENT_HITS" ]]; then
|
||||
echo -e "\n${RED}BLOCKED: Embedded secrets/tokens found in:${NC}"
|
||||
echo "$CONTENT_HITS" | while read -r f; do
|
||||
echo -e " ${RED}$f${NC}"
|
||||
# Show first matching line for context
|
||||
grep -nE "$SECRET_REGEX" "$f" 2>/dev/null | head -2 | while read -r line; do
|
||||
echo -e " ${YELLOW}$line${NC}"
|
||||
done
|
||||
done
|
||||
FOUND=1
|
||||
fi
|
||||
fi
|
||||
|
||||
rm -f "$FILELIST"
|
||||
|
||||
# ── Result ──────────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
if [[ $FOUND -eq 1 ]]; then
|
||||
echo -e "${RED}Secret scan FAILED. Add these to .gitignore or remove them before committing.${NC}"
|
||||
echo -e "${YELLOW}If intentional (e.g. test fixtures): git commit --no-verify${NC}"
|
||||
exit 1
|
||||
else
|
||||
echo -e "${GREEN}Secret scan passed. No keys or secrets detected.${NC}"
|
||||
exit 0
|
||||
fi
|
||||
@@ -0,0 +1,10 @@
|
||||
param(
|
||||
[string]$Python = "python"
|
||||
)
|
||||
|
||||
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
|
||||
$venvPath = Join-Path $repoRoot "venv"
|
||||
& $Python -m venv $venvPath
|
||||
|
||||
$pip = Join-Path $venvPath "Scripts\pip.exe"
|
||||
& $pip install -r (Join-Path $repoRoot "requirements-dev.txt")
|
||||
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
PYTHON="${PYTHON:-python3}"
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
VENV_DIR="$REPO_ROOT/venv"
|
||||
|
||||
"$PYTHON" -m venv "$VENV_DIR"
|
||||
"$VENV_DIR/bin/pip" install -r "$REPO_ROOT/requirements-dev.txt"
|
||||
Reference in New Issue
Block a user